diff --git a/.env b/.env new file mode 100644 index 00000000..01d799a1 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +TAG=v0.1.0 + +SERVER_NAME=federatedai/fedlcm-server +SERVER_IMG=${SERVER_NAME}:${TAG} + +FRONTEND_NAME=federatedai/fedlcm-frontend +FRONTEND_IMG=${FRONTEND_NAME}:${TAG} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..99cb1b3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.cfg +*.idea/ +*.vscode/ +*output/ +*.todo +*.exe +dist/ +node_modules/ +*.egg-info +*.test +.DS_Store +*.out +*.tgz +*.tar diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..ca51c751 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at contact@fedai.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a5ac0201 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing + +## Welcome +This guide provides FedLCM project contribution guidelines for open source contributors. **Please leave comments / suggestions if you find something is missing or incorrect.** + +When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. Please also follow the general FATE community [CONTRIBUTING.md](https://github.com/FederatedAI/FATE-Community/blob/master/CONTRIBUTING.md) guide. + +## Contribute Workflow +PR are always welcome, even if they only contain small fixes like typos or a few lines of codes. If there will be a significant effort, please: +1. Document it as a proposal; +2. PR it to https://github.com/FederatedAI/FATE-Community/tree/master/proposal; +3. Get a discussion by creating issues in this project and refer to the proposal; +4. Implement the feature when the proposal got 2+ maintainer approved, refer to the next sections on how to do it; +5. PR to this project's **current develop** branch. + +### Fork and clone +First, fork this repository on GitHub to your personal account and clone the code to your local workspace. + +Set user to match your GitHub profile name and clone the project: +``` +USER={your github profile name} +git clone https://github.com/FederatedAI/FedLCM.git && cd FedLCM + +# add your fork +git remote add $USER https://github.com/$USER/FedLCM.git + +git fetch -av +``` + +### Branch +Changes should be made on your own fork in a new branch. The branch should be named XXX-description where XXX is the number of the issue. PR should be rebased on top of main branch without multiple branches mixed into the PR. If your PR do not merge cleanly, use commands listed below to get it up to date. + +``` +#Suppose `origin` is the origin upstream + +git fetch origin +git checkout main +git rebase origin/main +``` +Branch from the updated main branch: +``` +git checkout -b my_feature main +``` + +### Keep sync with upstream +Once your branch gets out of sync with the origin/main branch, use the following commands to update: +``` +git checkout my_feature +git fetch -a +git rebase origin/main +Please use fetch / rebase (as shown above) instead of git pull. git pull does a merge, which leaves merge commits. These make the commit history messy and violate the principle that commits ought to be individually understandable and useful (see below). You can also consider changing your .git/config file via git config branch.autoSetupRebase always to change the behavior of git pull. +``` + +### Update the APIs and related documents +Our RESTful APIs are documented with [Swagger](https://swagger.io/) +If your commit that changes the RESTful APIs, make sure to run `make swag` to update the Swagger documents. + +### Commit +As FedLCM has integrated the [DCO (Developer Certificate of Origin)](https://probot.github.io/apps/dco/) check tool, contributors are required to sign-off that they adhere to those requirements by adding a Signed-off-by line to the commit messages. Git has even provided a -s command line option to append that automatically to your commit messages, please use it when you commit your changes. +``` +$ git commit -s -m 'This is my commit message' +``` +Commit your changes if they're ready: +``` +git add -A +git commit -s +git push --force-with-lease $USER my_feature +The commit message should follow the convention on How to Write a Git Commit Message. Be sure to include any related GitHub issue references in the commit message. +``` + +### Push and Create PR +When ready for review, push your branch to your fork repository on github.com: +``` +git push --force-with-lease $USER my_feature +``` +Then visit your fork at https://github.com/$USER/FedLCM and click the `Compare & Pull Request` button next to your `my_feature` branch to create a new pull request (PR). The PR should: +1. Ensure all unit test passed; +2. The tittle of PR should highlight what it solves briefly. The description of PR should refer to all the issues that it addresses. Ensure to put a reference to issues (such as `Close #xxx` and `Fixed #xxx`) Please refer to the [PULL_REQUEST_TEMPLATE.md](./PULL_REQUEST_TEMPLATE.md). + +Once your pull request has been opened it will be assigned to one or more reviewers. Those reviewers will do a thorough code review, looking for correctness, bugs, opportunities for improvement, documentation and comments, and style. + +Commit changes made in response to review comments to the same branch on your fork. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..252c327d --- /dev/null +++ b/Makefile @@ -0,0 +1,108 @@ +.PHONY: all clean format swag swag-bin server-unittest server frontend run upgrade openfl-device-agent release + +RELEASE_VERSION ?= ${shell git describe --tags} +TAG ?= v0.1.0 + +SERVER_NAME ?= federatedai/fedlcm-server +SERVER_IMG ?= ${SERVER_NAME}:${TAG} + +FRONTEND_NAME ?= federatedai/fedlcm-frontend +FRONTEND_IMG ?= ${FRONTEND_NAME}:${TAG} + +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +OUTPUT_DIR = output +RELEASE_DIR = ${OUTPUT_DIR}/release +ifeq ($(OS),Windows_NT) +BUILD_MODE = -buildmode=exe +OUTPUT_FILE = ${OUTPUT_DIR}/lifecycle-manager.exe +else +BUILD_MODE = +OUTPUT_FILE = ${OUTPUT_DIR}/lifecycle-manager +endif +OUTPUT_FRONTEND_FOLDER = ${OUTPUT_DIR}/frontend + +BRANCH = $(shell git symbolic-ref --short HEAD) +COMMIT = $(shell git log --pretty=format:'%h' -n 1) +NOW = $(shell date "+%Y-%m-%d %T UTC%z") + +LDFLAGS = "-X 'github.com/FederatedAI/FedLCM/server/constants.Branch=$(BRANCH)' \ + -X 'github.com/FederatedAI/FedLCM/server/constants.Commit=$(COMMIT)' \ + -X 'github.com/FederatedAI/FedLCM/server/constants.BuildTime=$(NOW)' \ + -extldflags '-static'" + + +all: swag server frontend openfl-device-agent + +frontend: + rm -rf ${OUTPUT_FRONTEND_FOLDER} + mkdir -p ${OUTPUT_DIR} + cd frontend && npm run build --prod + cp -rf frontend/dist/lifecycle-manager ${OUTPUT_FRONTEND_FOLDER} + +# Run go fmt & vet against code +format: + go fmt ./... + go vet ./... + +# Build manager binary +server: format + mkdir -p ${OUTPUT_DIR} + CGO_ENABLED=0 go build -a --ldflags ${LDFLAGS} -o ${OUTPUT_FILE} ${BUILD_MODE} server/main.go + +# Build the cmd line program +openfl-device-agent: format + mkdir -p ${OUTPUT_DIR} + CGO_ENABLED=0 go build -a --ldflags ${LDFLAGS} -o ${OUTPUT_DIR}/openfl-device-agent ${BUILD_MODE} cmd/device-agent/device-agent.go + +# Run server tests +server-unittest: format + go test ./... -coverprofile cover.out + +run: format + go run --ldflags ${LDFLAGS} ./server/main.go + +# Generate swag API file +swag: swag-bin + cd server && $(SWAG_BIN) fmt ;\ + $(SWAG_BIN) init --parseDependency --parseInternal + +swag-bin: +ifeq (, $(shell which swag)) + @{ \ + set -e ;\ + SWAG_BIN_TMP_DIR=$$(mktemp -d) ;\ + cd $$SWAG_BIN_TMP_DIR ;\ + go mod init tmp ;\ + go get -u github.com/swaggo/swag/cmd/swag ;\ + rm -rf $$SWAG_BIN_TMP_DIR ;\ + } +SWAG_BIN=$(GOBIN)/swag +else +SWAG_BIN=$(shell which swag) +endif + +docker-build: + docker build . -t ${SERVER_IMG} -f make/server/Dockerfile + docker build . -t ${FRONTEND_IMG} -f make/frontend/Dockerfile + +docker-push: + docker push ${SERVER_IMG} + docker push ${FRONTEND_IMG} + +clean: + rm -rf ${OUTPUT_DIR} + rm -rf frontend/dist + +upgrade: + go get -u ./... + +release: + rm -rf ${RELEASE_DIR} + mkdir -p ${RELEASE_DIR} + tar -czvf ${RELEASE_DIR}/fedlcm-k8s-${RELEASE_VERSION}.tgz rbac_config.yaml k8s_deploy.yaml + tar -czvf ${RELEASE_DIR}/fedlcm-docker-compose-${RELEASE_VERSION}.tgz .env docker-compose.yml make/stepca diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..86a5229a --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +Fixes ISSUE#xxx + +Changes: + +1. + +2. + +3. + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..20f61d40 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# FedLCM: Federation Lifecycle Manager + +A web application to manage FML deployments lifecycles + +## Deploy & Run Using Docker Compose +**System requirements:** On a Linux host with docker 18+ and docker-compose 1.24+ . +* Download the `fedlcm-docker-compose-.tgz` package from the releases pages, unzip the files into a folder. Or clone the whole project and execute in the project root folder. +* **(Optional)** modify `.env` file to change the image names. Only do this if you want to use your customized registry or images. +* Then +``` +docker-compose pull +docker-compose up +``` +By default, the web UI is exposed on the 9080 port of the machine. Access that address from a browser to open the FedLCM's UI. + +## Deploy Into Kubernetes +* Download the `fedlcm-k8s-.tgz` package from the releases pages, and unzip the files into a folder. Or clone the whole project and execute in the project root folder. +* **(Optional)** We recommend using persistent storage to avoid the change of CA root certificate and the loss of data when restarting the deployment. To enable persistent volume, you should create your `StorageClass` and corresponding provisioner first. Then modify the commented section in the `k8s_deploy.yaml` to use `persistentVolumeClaim`. Replace with your storage class name at `storageClassName`. +* Apply the yaml files in the follow order +``` +kubectl apply -f rbac_config.yaml +kubectl apply -f k8s_deploy.yaml +``` +By default, the web UI is exposed via a NodePort service that listens on port 30008 of the nodes. After all resources are successfully created and running, you can enter into your FedLCM service use `:30008`. Alternatively you can change the Service definition in the yaml to use your preferred service type and exposed port. + +## Getting Started + +Refer to the [getting started guide](./doc/Getting_Started_FATE.md) for FATE federation management. + +## Development +### Build +``` +make all +``` +The generated deliverables are placed in the `output` folder. + +### Build & Run Docker Image +* Modify `.env` file to change the image name, and then +``` +set -a; source .env; set +a +make docker-build +``` +* Optionally push the image +``` +make docker-push +``` +* Start the service +``` +docker-compose up +``` + +Refer to the [development guide](./doc/Development_Guide.md) for more development related topics. + +## License + +FedLCM is available under the [Apache 2 license](LICENSE). + +This project uses open source components which have additional licensing terms. The official docker images and licensing terms for these open source components can be found at the following locations: + +* Photon OS: [docker image](https://hub.docker.com/_/photon/), [license](https://github.com/vmware/photon/blob/master/COPYING) \ No newline at end of file diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 00000000..0423b1e2 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,76 @@ +# FedLCM: Federation Lifecycle Manager + +FedLCM是一个基于Web的联邦学习的联邦生命周期管理服务,支持联邦学习组件和应用的部署管理,联邦网络创建以及底层基础设施的安装配置等。 + +## 使用 Docker Compose 部署 + +**系统要求**:安装有 docker 18+ 以及 docker-compose 1.24+ 的 Linux 系统 + +* 在 release 页面下载 `fedlcm-docker-compose-.tgz` 安装包,并解压到指定文件夹。或者直接在本仓库目录下执行如下操作。 +* **(可选)** 若使用自定义的镜像仓库或镜像,请修改 `.env` 文件中的镜像名称。 +* 执行如下命令开启应用: + +```shell +docker-compose pull +docker-compose up +``` + +应用成功开启后可通过主机地址及服务端口号(默认为 9080)访问 FedLCM 的网页。 + +## 部署至 Kubernetes 集群 + +* 在 release 界面下载 `fedlcm-k8s-.tgz` 并解压至指定文件夹。或者直接在本仓库目录下执行如下操作。 +* **(可选)** 建议使用持久化存储来避免 CA 根证书发生变化或者重启服务之后数据丢失。可以创建 `StorageClass` 以及相应的 provisioner,然后修改 `k8s_deploy.yaml` 中相关的注释内容来开启 `persistentVolumeClaim` 。注意需要替换 `storageClassName` 部分的值。 +* 执行如下指令完成部署: + +```shell +kubectl apply -f rbac_config.yaml +kubectl apply -f k8s_deploy.yaml +``` + +Web 界面默认使用 NodePort 服务并监听 30008 端口。待所有资源都成功创建并运行后,可以通过 `:30008` 访问 FedLCM 界面。如需修改相关配置,请自行调整上述 YAML 文件的内容。 + +## 快速开始 + +参见 [FATE 联邦管理指南](./doc/Getting_Started_FATE_zh.md)。 + +## 本地开发 + +### Build + +```shell +make all +``` + +生成的文件默认存放在 `./output` 目录下。 + +### 打包并运行 Docker 镜像 + +* 修改 `.env` 文件中的镜像名称,然后执行: + +```shell +set -a; source .env; set +a +make docker-build +``` + +* 可以使用如下命令快捷推送镜像 + +```shell +make docker-push +``` + +* 开启服务 + +```shell +docker-compose up +``` + +详情参见 [FedLCM 本地开发指南](./doc/Development_Guide_zh.md)。 + +## License + +FedLCM 使用 [Apache 2 license](LICENSE). + +本项目使用了有其他附加条款的开源组件,关于其官方镜像以及授权条款的详细信息参见如下链接: + +* Photon OS: [docker image](https://hub.docker.com/_/photon/), [license](https://github.com/vmware/photon/blob/master/COPYING) diff --git a/cmd/device-agent/cli/common.go b/cmd/device-agent/cli/common.go new file mode 100644 index 00000000..b81f97cc --- /dev/null +++ b/cmd/device-agent/cli/common.go @@ -0,0 +1,74 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/FederatedAI/FedLCM/pkg/utils" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func sendJSON(method, urlStr string, body interface{}) (*http.Response, error) { + var resp *http.Response + if err := utils.RetryWithMaxAttempts(func() error { + var payload []byte + if stringBody, ok := body.(string); ok { + payload = []byte(stringBody) + } else { + var err error + if payload, err = json.Marshal(body); err != nil { + return err + } + } + req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + log.Debugf("%s request to %s with body %s", method, urlStr, string(payload)) + resp, err = http.DefaultClient.Do(req) + return err + }, 3, 10*time.Second); err != nil { + return nil, err + } + return resp, nil +} + +func getUrl(host string, port int, tls bool, path string) string { + schemaStr := "http" + if tls { + schemaStr = "https" + } + return fmt.Sprintf("%s://%s:%v/api/v1/%s", schemaStr, host, port, path) +} + +func parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Wrap(err, "read response body error") + } + log.Debugf("response body: %v", string(body)) + if response.StatusCode != http.StatusOK { + return body, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} diff --git a/cmd/device-agent/cli/register.go b/cmd/device-agent/cli/register.go new file mode 100644 index 00000000..b3cc6319 --- /dev/null +++ b/cmd/device-agent/cli/register.go @@ -0,0 +1,196 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "encoding/json" + "io/ioutil" + "path/filepath" + "time" + + "github.com/FederatedAI/FedLCM/pkg/utils" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" + "k8s.io/client-go/util/homedir" +) + +type envoyRegistrationConfig struct { + Name string `json:"name"` + Description string `json:"description"` + Namespace string `json:"namespace"` + ChartUUID string `json:"chart_uuid" yaml:"chartUUID"` + Labels valueobject.Labels `json:"labels"` + SkipCommonPythonFiles bool `json:"skip_common_python_files" yaml:"skipCommonPythonFiles"` + EnablePSP bool `json:"enable_psp" yaml:"enablePSP"` + RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config" yaml:"registryConfig"` +} + +type envoyRegistrationRequest struct { + // required + KubeConfig string `json:"kubeconfig"` + TokenStr string `json:"token"` + + // optional + envoyRegistrationConfig + ConfigYAML string `json:"config_yaml"` +} + +func RegisterCommand() *cli.Command { + return &cli.Command{ + Name: "register", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "extra-config", + Usage: "YAML file containing optional extra configuration for the register command", + Required: false, + }, + &cli.StringFlag{ + Name: "kube-config", + Aliases: []string{"k"}, + Value: filepath.Join(homedir.HomeDir(), ".kube", "config"), + Usage: "Kubeconfig file", + Required: false, + }, + &cli.StringFlag{ + Name: "envoy-config", + Usage: "Optional Envoy configuration file path", + Required: false, + }, + &cli.StringFlag{ + Name: "server-host", + Aliases: []string{"s"}, + Value: "", + Usage: "Host address of the lifecycle manager", + Required: true, + }, + &cli.IntFlag{ + Name: "server-port", + Aliases: []string{"p"}, + Usage: "Port number of the lifecycle manager", + Required: true, + }, + &cli.BoolFlag{ + Name: "tls", + Value: false, + Usage: "Enable TLS", + Required: false, + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Value: "", + Usage: "The registration token string", + Required: true, + }, + &cli.BoolFlag{ + Name: "wait", + Aliases: []string{"w"}, + Value: false, + Usage: "Wait for the registration to finished", + Required: false, + }, + }, + Usage: "Register to the lifecycle manager", + Action: func(c *cli.Context) error { + + req := &envoyRegistrationRequest{ + TokenStr: c.String("token"), + } + + kubeConfigPath := c.String("kube-config") + kubeconfigBytes, err := ioutil.ReadFile(kubeConfigPath) + if err != nil { + return err + } + var m map[string]interface{} + if err := yaml.Unmarshal(kubeconfigBytes, &m); err != nil { + return errors.Wrap(err, "invalid kube config") + } + req.KubeConfig = string(kubeconfigBytes) + + envoyConfigPath := c.String("envoy-config") + if envoyConfigPath != "" { + envoyConfigBytes, err := ioutil.ReadFile(envoyConfigPath) + if err != nil { + return err + } + var m map[string]interface{} + if err := yaml.Unmarshal(envoyConfigBytes, &m); err != nil { + return errors.Wrap(err, "invalid envoy config") + } + req.ConfigYAML = string(envoyConfigBytes) + } + + configPath := c.String("extra-config") + if configPath != "" { + configBytes, err := ioutil.ReadFile(configPath) + if err != nil { + return err + } + log.Debugf("ReadFile Success, yaml: %s", string(configBytes)) + config := envoyRegistrationConfig{} + if err := yaml.Unmarshal(configBytes, &config); err != nil { + return err + } + req.envoyRegistrationConfig = config + } + + resp, err := sendJSON("POST", + getUrl(c.String("server-host"), c.Int("server-port"), c.Bool("tls"), "federation/openfl/envoy/register"), + req) + defer resp.Body.Close() + body, err := parseResponse(resp) + if err != nil { + return err + } + type RegisterResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data"` + } + var registerResponse RegisterResponse + if err := json.Unmarshal(body, ®isterResponse); err != nil { + return err + } + log.Infof("New Envoy is being prepared, UUID: %s", registerResponse.Data) + if c.Bool("wait") { + log.Infof("Waiting for the preparation to finish") + if err := utils.ExecuteWithTimeout(func() bool { + envoy, err := getEnvoyInfo(req.TokenStr, registerResponse.Data, c.String("server-host"), c.Int("server-port"), c.Bool("tls")) + if err != nil { + log.Errorf("error getting Envoy info: %v, retry", err) + return false + } + log.Infof("Envoy %s(%s) status is: %v", envoy.Name, envoy.UUID, envoy.Status) + if envoy.Status == entity.ParticipantOpenFLStatusFailed || + envoy.Status == entity.ParticipantOpenFLStatusActive || + envoy.Status == entity.ParticipantOpenFLStatusUnknown || + envoy.Status == entity.ParticipantOpenFLStatusRemoving { + return true + } + return false + }, time.Hour, time.Second*2); err != nil { + return err + } + log.Infof("Preparation finished") + } + return nil + }, + } +} diff --git a/cmd/device-agent/cli/root.go b/cmd/device-agent/cli/root.go new file mode 100644 index 00000000..fbea713a --- /dev/null +++ b/cmd/device-agent/cli/root.go @@ -0,0 +1,59 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "sort" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" // imports as package "cli" +) + +func init() { + cli.HelpFlag = &cli.BoolFlag{Name: "help", Aliases: []string{"h"}, Usage: "Show help"} +} + +func initCommandLine() *cli.App { + app := &cli.App{ + + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Value: false, + Usage: "Enable debug logging", + Required: false, + }, + }, + Commands: []*cli.Command{ + RegisterCommand(), + StatusCommand(), + }, + } + app.Before = func(c *cli.Context) error { + if c.Bool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + return nil + } + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + return app +} + +func Run(Args []string) error { + app := initCommandLine() + return app.Run(Args) +} diff --git a/cmd/device-agent/cli/status.go b/cmd/device-agent/cli/status.go new file mode 100644 index 00000000..de1a3bd2 --- /dev/null +++ b/cmd/device-agent/cli/status.go @@ -0,0 +1,100 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + + "github.com/FederatedAI/FedLCM/server/application/service" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func StatusCommand() *cli.Command { + return &cli.Command{ + Name: "status", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "server-host", + Aliases: []string{"s"}, + Value: "", + Usage: "Host address of the lifecycle manager", + Required: true, + }, + &cli.IntFlag{ + Name: "server-port", + Aliases: []string{"p"}, + Usage: "Port number of the lifecycle manager", + Required: true, + }, + &cli.BoolFlag{ + Name: "tls", + Value: false, + Usage: "Enable TLS", + Required: false, + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Value: "", + Usage: "The registration token string", + Required: true, + }, + &cli.StringFlag{ + Name: "uuid", + Aliases: []string{"u"}, + Value: "", + Usage: "The envoy UUID", + Required: true, + }, + }, + Usage: "Query the status of the Envoy", + Action: func(c *cli.Context) error { + token := c.String("token") + uuid := c.String("uuid") + envoy, err := getEnvoyInfo(token, uuid, c.String("server-host"), c.Int("server-port"), c.Bool("tls")) + if err != nil { + return err + } + log.Infof("Envoy status is: %v", envoy.Status) + return nil + }, + } +} + +func getEnvoyInfo(token, uuid, host string, port int, tls bool) (*service.OpenFLEnvoyDetail, error) { + resp, err := sendJSON("GET", getUrl(host, port, tls, fmt.Sprintf("federation/openfl/envoy/%s?token=%s", uuid, token)), nil) + defer resp.Body.Close() + body, err := parseResponse(resp) + if err != nil { + return nil, err + } + + type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data *service.OpenFLEnvoyDetail `json:"data"` + } + var response Response + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + if response.Data == nil || response.Data.UUID != uuid { + return nil, errors.Errorf("invalid response: %v", response) + } + return response.Data, nil +} diff --git a/cmd/device-agent/device-agent.go b/cmd/device-agent/device-agent.go new file mode 100644 index 00000000..aece1ca9 --- /dev/null +++ b/cmd/device-agent/device-agent.go @@ -0,0 +1,30 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + + "github.com/FederatedAI/FedLCM/cmd/device-agent/cli" + log "github.com/sirupsen/logrus" +) + +func main() { + err := cli.Run(os.Args) + if err != nil { + log.Error(err) + os.Exit(1) + } +} diff --git a/doc/Development_Guide.md b/doc/Development_Guide.md new file mode 100644 index 00000000..31737852 --- /dev/null +++ b/doc/Development_Guide.md @@ -0,0 +1,126 @@ +# Guide for Local Development of FedLCM + +## Requirements + +In this section you can have a glance of the minimum and recommended versions of the tools needed to build/debug FedLCM. + +These tools and services needs to be installed: + +| Tool | Link | Minimum | Recommended | +|---|---|---|---| +| npm | [link](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) | >= 6 | >= 7 | +| NodeJS | [link](https://nodejs.org/en/) | >= 14 | >= 16 | +| golang | [link](https://go.dev/dl/) | >= 1.17 | >= 1.17 | + +## Service Dependency + +FedLCM depends on a [PostgreSQL](https://www.postgresql.org/docs/current/) Database and a [StepCA](https://smallstep.com/docs/step-ca/getting-started) service. Refer to their documents on how to setup one. + +## Quick Setup and Run + +Clone the repository: + +```shell +git clone $URL +``` + +### Start Backend Server +Configure necessary environment variables for connecting with PostgreSQL: + +| Name | Description | Required | +|---|---|---| +| POSTGRES_HOST | the address of the postgres db | Yes | +| POSTGRES_PORT | the port of the postgres db | Yes | +| POSTGRES_USER | the username of the postgres connection | Yes | +| POSTGRES_PASSWORD | the password of the postgres connection | Yes | +| POSTGRES_DB | the db name of the postgres | Yes | + +Go to `./server` directory and run: + +```shell +go run main.go +``` + +If the process goes well, backend server will listen and serving HTTP on port `8080`. + +Alternatively, you can configure your favorite IDE to start this service. + +### Start Frontend Server + +1. Run `npm install` under "frontend" directory. +2. create "proxy.config.json" file under "frontend" directory, and replace the URL of target with your available backend server. +``` + { + "/api/v1": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "headers": { + "Connection": "keep-alive" + } + } + } +``` +3. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +4. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +> Default username is `Admin` with password `admin`. + +### Other Useful Commands + +```shell +# Build both frontend and backend into `./output` directory. +make all + +# Run server tests +make server-unittest + +# Run frontend tests +cd frontend && npm test + +# Build docker image +make docker-build + +# Push docker image +make docker-push +``` + +## Other configurable environment variables + +| Name | Description | Required | +|---|---|---| +| POSTGRES_DEBUG | whether or not to enable postgres debug level logs | No, default to false | +| POSTGRES_SSLMODE | whether or not to enable ssl connection to DB | No, default to false | +| LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD | initial admin user password, only takes effect on first start | No, default to "admin" | +| LIFECYCLEMANAGER_SECRETKEY | a string of secret key for encrypting sensitive data in the DB | No, default to "passphrase123456" | +| LIFECYCLEMANAGER_DEBUG | true or false to enable debug log | No, default to false | +| LIFECYCLEMANAGER_EXPERIMENT_ENABLED | true of false to enable OpenFL management | No, default to false | +| LIFECYCLEMANAGER_JWT_KEY | a string of secret key for generating JWT token | No, default to a random one | + +## Development + +### Frontend + +The frontend is based on [Clarity](https://clarity.design/) and [Angular](https://angular.io/). + +### Backend + +We use [Gin framework](https://github.com/gin-gonic/gin) to handle API requests and [GORM](https://gorm.io/index.html) to persist data. + +The code are organized as below: + +```C +pkg +├── kubefate // KubeFATE management and client code +├── kubernetes // K8s client code +└── utils // some basic util functions +server +├── api // Gin route and API handlinig +├── application // App services called by the handlers in api level +├── constants // some constants +├── docs // swagger docs +├── domain // domain driven design implementation of the main workflow +├── infrastructure // client to other system, GORM logics etc. that can be used by other layers +└── main.go // entry point +``` \ No newline at end of file diff --git a/doc/Development_Guide_zh.md b/doc/Development_Guide_zh.md new file mode 100644 index 00000000..fd1ad234 --- /dev/null +++ b/doc/Development_Guide_zh.md @@ -0,0 +1,127 @@ +# FedLCM 本地开发指南 + +## 环境要求 + +本地搭建 FedLCM 开发环境所需工具最低版本要求以及推荐版本如下: + +| 开发工具 | 下载链接 | 最低版本 | 推荐版本 | +|---|---|---|---| +| npm | [link](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) | >= 6 | >= 7 | +| NodeJS | [link](https://nodejs.org/en/) | >= 14 | >= 16 | +| golang | [link](https://go.dev/dl/) | >= 1.17 | >= 1.17 | + +## 其他依赖服务 + +运行 FedLCM 需要提前部署 [PostgreSQL](https://www.postgresql.org/docs/current/) 数据库服务和 [StepCA](https://smallstep.com/docs/step-ca/getting-started) 证书服务。详情参见相关文档。 + +## 快速开始 + +将代码仓库克隆到本地: + +```shell +git clone $URL +``` + +### 启动后端服务 + +配置连接数据库所需的环境变量: + +| 变量名 | 备注 | 是否必需 | +|---|---|---| +| POSTGRES_HOST | postgres 数据库地址 | 是 | +| POSTGRES_PORT | postgres 数据库端口 | 是 | +| POSTGRES_USER | 数据库用户名 | 是 | +| POSTGRES_PASSWORD | 数据库密码 | 是 | +| POSTGRES_DB | 数据库名称 | 是 | + +在命令行切换到 `./server` 目录并执行如下指令: + +```shell +go run main.go +``` + +后端服务成功运行后默认监听 `8080` 端口。 + +除使用命令行外,也可以用你熟悉的 IDE 来开启后端服务。 + +### 启动前端服务 + +1. 在 `./frontend` 目录下执行 `npm install` 指令。 +2. 在 `./frontend` 目录下创建“proxy.config.json”文件并将其中的“target”项替换为后端服务的地址。 + +```json + { + "/api/v1": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "headers": { + "Connection": "keep-alive" + } + } + } +``` + +1. 执行 `ng serve` 指令开启前端服务,打开 `http://localhost:4200/` 即可访问。如果源文件发生变化,服务会自动重启。 +2. 执行 `ng build` 指令构建前端项目。生成的文件默认存放在 `dist/` 目录下。 + +> 默认用户名为`Admin`,密码为`admin`。 + +### 其他常用指令 + +```shell +# 将前后端服务打包至“./output”目录下 +make all + +# 运行单元测试 +make server-unittest + +# 运行前端测试 +cd frontend && npm test + +# 构建 docker 镜像 +make docker-build + +# 推送 docker 镜像 +make docker-push +``` + +## 其他环境变量 + +| 变量名 | 备注 | 是否必需 | +|---|---|---| +| POSTGRES_DEBUG | 是否开启 postgres 数据库 debug 级别日志 | 否,默认为 false | +| POSTGRES_SSLMODE | 是否使用 ssl 连接数据库 | 否,默认为 false | +| LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD | 初始 admin 账户密码,只在第一次启动时生效 | 否,默认为 "admin" | +| LIFECYCLEMANAGER_SECRETKEY | 加密数据库敏感数据所用密钥 | 否,默认为 "passphrase123456" | +| LIFECYCLEMANAGER_DEBUG | 是否开启 debug 级别日志 | 否,默认为 false | +| LIFECYCLEMANAGER_EXPERIMENT_ENABLED | 是否开启 OpenFL 管理服务 | 否,默认为 false | +| LIFECYCLEMANAGER_JWT_KEY | 生成 JWT token 的密钥 | 否,默认为随机值 | + +## 技术栈简介 + +### 前端项目 + +前端基于 [Clarity](https://clarity.design/) 和 [Angular](https://angular.io/) 实现。 + +### 后端项目 + +使用 [Gin framework](https://github.com/gin-gonic/gin) 处理 API 请求,使用 [GORM](https://gorm.io/index.html) 持久化数据。 + +代码目录结构如下: + +```C +pkg +├── kubefate // KubeFATE management and client code +├── kubernetes // K8s client code +└── utils // some basic util functions +server +├── api // Gin route and API handlinig +├── application // App services called by the handlers in api level +├── constants // some constants +├── docs // swagger docs +├── domain // domain driven design implementation of the main workflow +├── infrastructure // client to other system, GORM logics etc. that can be used by other layers +└── main.go // entry point +``` diff --git a/doc/Getting_Started_FATE.md b/doc/Getting_Started_FATE.md new file mode 100644 index 00000000..674c6cac --- /dev/null +++ b/doc/Getting_Started_FATE.md @@ -0,0 +1,207 @@ +# Getting Started Guide for FATE Federations Management + +## Overall Architecture and Steps + +This document provides an end-to-end guide to creating a FATE federation using FedLCM, i.e., the Federation Lifecycle Manager service. Currently, FATE instances deployed here use Spark and Pulsar as the backend and connect with each other via an exchange. +The overall deployment architecture is below diagram: + +
+ +
+ +The high-level steps are: + +1. FedLCM connects to Kubernetes clusters via corresponding kubeconfig. +2. It then installs KubeFATE services onto K8s clusters as endpoints. +3. Federation entities can be created as a logical container for future FATE deployment. +4. For one federation, FedLCM firstly deploys a FATE exchange into a K8s cluster with the help of KubeFATE. +5. Then, it creates several FATE clusters as different parties into different K8s clusters, also by working with KubeFATE services. +6. Jupyter Notebook is created for each FATE cluster, and users can start using it to start FML jobs. +7. Alternatively, for each FATE v1.6.1 deployment, FedLCM provides a management portal called "Site Portal" that can also be used to manage FATE jobs. + +## Prerequisites + +FedLCM works with Kubernetes clusters to deploy KubeFATE and FATE. It can accelerate the overall workflow and enable more features if your cluster has applications like Ingress Controllers, StorageClass Provisioners and LoadBalancer Providers installed. But it isn't a hard requirement for minimal FATE federation deployments. + +> Currently, FedLCM needs cluster-admin privilege to manage the FATE deployments. + +## Deployment + +The FedLCM service can be deployed via docker-compose or in a K8s cluster. Refer to the [README](../README.md) doc for the steps. + +After deploying the FedLCM service, access the web UI from a browser: +
+ +
+ +The login credential can be configured via modifying the docker-compose yaml or the k8s_deploy yaml. The default is `Admin:admin`. + +## Configuring CA + +The FedLCM service depends on a CA service to issue certificates for the deployed components. To configure one, go to the Certificate section and click the `NEW` button. Currently, this service can work with a StepCA server. And both the docker-compose deployment and the K8s deployment contain a built-in StepCA server that can be used directly. + +
+ +
+ +Click `SUBMIT` to save the CA configuration. + +## Adding Infrastructure + +Kubernetes clusters are considered as Infrastructures in the FedLCM service. All the other installation activities are performed on these K8s clusters. To deploy other services, including KubeFATE and FATE clusters, you must add the target K8s cluster as infrastructure. + +Go to the `Infrastructure` section and click the `NEW` button. Provide some basic information of this infrastructure, especially the kubeconfig content, which FedLCM will use to connect to the K8s cluster. + +> **The user configured in the kubeconfig section should have the privilege to create all core K8s resources, including namespace, deployment, configmap, role, secret, etc.** + +
+ +
+ +Click `TEST` to make sure the cluster is available. Then click `SUBMIT` to save the new infrastructure. + +> You can toggle the switch of *Configure Registry* to user your own image registry. It will be used as the default registry for future FATE deployment on this K8s cluster. If not configured, images will by default be fetched from Docker Hub. +> +> You can also optionally enable *Use Registry Secret* to provide your registry credentials. + +In practical use, there will be several K8s clusters from different organizations. In this guide, we added two clusters to demonstrate a minimal workflow. + +## Adding or Installing KubeFATE + +In the `Endpoint` section, you can install KubeFATE service into a K8s infrastructure. And later it can be used to deploy FATE components. + +To add a new KubeFATE endpoint, select an existing infrastructure and FedLCM will try to find if there is already a KubeFATE service running. If so, it will add the KubeFATE service into its database directly. If not, an installation step will be provided as shown below: + +
+ +
+ +Click `GET KUBEFATE INSTALLATION YAML` to get the deployment yaml file. You can make changes to the yaml content to further customize your KubeFATE installation, but the default one is typically sufficient. + +We depend on *Ingress* and *Ingress Controllers* to work with the KubeFATE service. You can optionally click the `Install an Ingress Controller for me` checkbox to install an ingress-nginx controller with some predefined configurations. + +**Only select the LoadBalancer service type if you know your cluster can support that.** + +
+ +
+ +Click `SUBMIT` and wait for the endpoint status to become ready. Check the event tab of the endpoint to see its detailed deployment events. And perform the same steps to install KubeFATE in other infrastructures. + +>An KubeFATE endpoint can only work with the K8s cluster it runs on. All the target K8s clusters should have their own KubeFATE service installed. + +## Creating Federations + +Now, on the `Federation` page, we can create new federations. Just click `NEW` and provide some basic information. + +
+ +
+ +The domain field will be used to generate the hostnames for the FATE and exchange components created in this federation. For example, the Pulsar service will use a certificate that indicates its name to be `.`. And ingress addresses are also generated based on this domain field. In this guide we input `example.com` as the base domain name. + +## Creating Participants + +### Creating Exchange + +In a just created federation, we firstly need an exchange. Click the `NEW` button under *Exchange* section in *FATE Federation Detail* page. And follow the steps to create a new exchange. + +
+ +
+ +> Note: +> * To use the FedLCM's Site Portal service, choose the “chart for FATE exchange v1.6.1 with fml-manager service” when selecting the chart. Otherwise, this federation cannot use Site Portal even if the FATE cluster contains one. +> * It is suggested to choose `Install certificates for me` in *the Certificate* section. Only select `I will manually install certificates` if you want to import your own certificate instead of using the configured CA to create a new one. +> * Choose `NodePort` if your cluster doesn't have any controller that can handle `LoadBalancer` type of service. +> * If your cluster doesn't enable [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy/), you don't have to enable it in *Pod Security Policy Configuration* section. + +Finally, get the yaml content, verify it is correct and click `SUBMIT`. + +Now, the FedLCM system will start installing the exchange, which is listed in the corresponding section: + +
+ +
+ +### Creating Clusters + +After the exchange's status becomes `Active`, FATE clusters could be added to the federation. Click the `NEW` button under the *Cluster* section and follow the steps to add a new FATE cluster. + +
+ +
+ +> Most of the steps are the same as configuring an exchange; see more in the note above. +> Choose the "chart for FATE cluster v1.6.1 with site-portal" chart to deploy FATE clusters with site-portal support. + +Finally, get the yaml content, verify it is correct and click `SUBMIT`. + +Creating a cluster may take more time than creating an exchange. You can go to *Cluster Detail* page by clicking a certain cluster name to access more information and some useful logs are provided in the *event* tab. + +## Run FATE Jobs + +Here we have created two FATE clusters called `cluster-01` and `cluster-02` correspondingly with party id `9999` and `10000`. + +
+ +
+ +By clicking a certain cluster name, cluster detail will show in a new page: + +
+ +
+ +If the FATE cluster contains a Site Portal service, it will be shown in the *Exposed Service* section, and we can click into it to work with our FML management portal. Refer to the [portal's guide](./Site_Portal_In_FedLCM_Configuration_Guide.md) on the usage. + +We can see some other core services in *Ingress Info* section and the most important two are `client` and `fateboard`. Client service is used for creating a FATE job and fateboard service is used for monitoring jobs on FATE clusters. + +We will use the address in the Hosts field to access these services from a web browser. In order to do that, these addresses should be resolved to the ingress controller's expose service IP address. This can be done by either changing your machine's *hosts* file or configuring your DNS server. In most of the cases, the target IP is the IP address in the Addresses column. + +**But if your ingress controller's service is exposed as `NodePort`, then you should use your node's IP, which might not be the address shown in the table.** Below example shows how to work with an ingress controller that is exposed as `NodePort`. + +``` +# Connect to your k8s cluster and get the information of ingress controller service by executing: +kubectl get svc -n ingress-nginx ingress-nginx-controller + + +# We can see the port exposed in the message: +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +ingress-nginx-controller NodePort 10.103.193.113 80:30637/TCP,443:32069/TCP 119m + +# Then on the node we want to accept the connnection, manually redirect the connection on port 80 of this node to this service by executing (Replace `` with the actual exposed port number(30637 in this case).): +nohup socat TCP-LISTEN:80,fork TCP:localhost: > /dev/null 2>&1 & +``` + +Now, we can access FATE notebook page with the host address of client service in *Ingress Info* section and [run FATE job, like examples from KubeFATE project](https://github.com/FederatedAI/KubeFATE/blob/master/docs/tutorials/Build_Two_Parties_FATE_Cluster_in_One_Linux_Machine_with_MiniKube.md#run-a-fate-example) through your web browser: + +
+ +
+ +And access fateboard page to see the current status of the running jobs via the host address of fateboard service. + +
+ +
+ +If everything goes well, the progress of the job should reach 100%, which means the federation can work as expected. And you can see more details in the job summary as below: + +
+ +
+ +> Alternatively, if we deployed FATE v1.8.0, then we can use `flow test toy` command to run a toy example test to verify everything works fine. + +## Destroying the Federation + +To delete a federation, we need to remove all clusters in the federation first, then remove the exchange, and finally just click `delete` in `Federation` section to destroy the federation. + +And to delete an infrastructure, all endpoints in the k8s cluster need to be removed (which requires all FATE deployments be uninstalled firstly). + +## Next Steps + +1. The example shown above is the simplest use of FedLCM. You can explore more functions and feedback are welcome. +2. Default yaml files were used in the example, you can customize them according to your needs. +3. When deploying FATE v1.6.1 with Site Portal service, we can create FATE jobs via this web service. For more detailed introduction of that service, follow the document [here](./Site_Portal_In_FedLCM_Configuration_Guide.md). diff --git a/doc/Getting_Started_FATE_zh.md b/doc/Getting_Started_FATE_zh.md new file mode 100644 index 00000000..d8b84747 --- /dev/null +++ b/doc/Getting_Started_FATE_zh.md @@ -0,0 +1,194 @@ +# FATE 联邦管理指南 + +## 总体架构及流程概览 + +本文档主要介绍如何使用 FedLCM(即 Federation Lifecycle Manager)创建 FATE 联邦并部署FATE实例。目前FedLCM系统支持部署的FATE实例都是以Spark和Pulsar作为基础引擎,并使用Exchange实现互相连接的。 总体部署架构如下: + +
+ +
+ +部署流程可以概括为如下几步: + +1. FedLCM 依据 kubeconfig 文件中的相关信息连接到 Kubernetes 集群。 +2. FedLCM 在各个 K8s 集群上安装 KubeFATE 作为服务端点。 +3. 之后用户可以通过FedLCM创建一个甚至多个“联邦”。 +4. 对于某个特定的联邦,FedLCM 通过 KubeFATE 首先在指定的 K8s 集群上部署一个 FATE exchange 服务。 +5. 然后 FedLCM 可以在不同的 K8s 集群上创建多个 FATE cluster 作为不同的参与方。 +6. 每个 FATE cluster 都会创建对应的 Jupyter Notebook,其可以用来创建 FML jobs。 +7. 特别的,对于每个部署的是 FATE v1.6.1 的 cluster,FedLCM 还提供了名为“Site Portal”的服务来方便用户管理FATE的数据、任务、项目等。 + +## 前置要求 + +FedLCM 部署 KubeFATE 和 FATE 都是在 Kubernetes 集群中进行。如果您的K8s集群中已经安装有 Ingress Controllers、StorageClass Provisioners 和 LoadBalancer Providers 等服务,那么 FedLCM 能够利用这些服务来提供更丰富的功能和加速FATE的部署。但对于最简 FATE 联邦的部署,这些额外的服务并不是必须的。 + +> 目前 FedLCM 需要 [cluster-admin 权限和角色](https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/rbac/#user-facing-roles) 来部署以及管理 FATE和 KubeFATE。 + +## 部署 FedLCM + +FedLCM 服务本身的部署支持 docker-compose 运行的方式或者在 K8s 集群中部署。详细步骤参见 [README](../README.md) 文档。 + +成功部署 FedLCM 服务后, 用户可通过浏览器访问其 Web 界面: + +
+ +
+ +登录口令可以在 docker-compose 的 yaml 文件或者 k8s_deploy yaml 文件中进行配置。默认口令为 `Admin:admin`。交互界面默认语言为英文,登录成功后可以通过右上角的菜单切换语言。 + +## 配置 CA + +FedLCM 服务需要通过一个 CA 服务来向各组件签发证书。因此我们需要配置它与一个 CA 服务的连接:在证书页面点击“新建”按钮添加新的证书颁发机构。目前使用 docker-compose 部署 或者 K8s 部署出来的 FedLCM 都会默认内置一个可以直接使用的 StepCA 服务,本文档直接使用这个内置的 CA。 + +
+ +
+ +点击 `提交` 保存配置。 + +## 添加基础设施 + +在 FedLCM 服务中,Kubernetes 集群被称作基础设施,所有安装活动都是在这些集群上进行。要部署 KubeFATE 和 FATE,必须将目标 K8s 集群添加为基础设施。 + +切换到“基础设施”页面点击`新建`按钮,填写该基础设施的一些信息,特别是 kubeconfig 文件的内容。 + +> **kubeconfig 中所指定的用户必须有创建相关 K8s 资源的权限, 如 namespace, deployment, configmap, role, secret 等,一般来说只有cluster-admin角色能够有这些权限。** + +
+ +
+ +点击`测试`来确认 FedLCM 可以正常连接到该集群。确认无误后点击 `提交` 保存。 + +> 可以打开“配置Registry”开关来自定义镜像仓库。之后的部署过程中FATE容器镜像将从这里配置的Registry中拉取。若保留默认配置,则从 DockerHub 拉取。 +> +> 同样可以打开“使用Registry Secret”来配置 registry 的访问凭证。 + +实际应用中会有来自不同组织的多个 K8s 集群,本文档仅添加两个集群来展示最简使用流程。 + +## 安装 KubeFATE + +在 `服务端点` 页面,点击 `新建` 以在 K8s 集群上安装 KubeFATE 服务。 + +首先选则列表中的一个基础设施,FedLCM 会检测该集群上是否已经安装 KubeFATE。若检测到 KubeFATE 已成功安装并在运行,则直接将其添加至数据库中,否则会执行如下安装步骤: + +
+ +
+ +按照提示配置相关内容,最终生成部署所用 YAML 文件。如需个性化修改默认生成的 YAML 文件,直接在文本框中编辑即可。一般来说默认的 YAML 内容就足够了。 + +KubeFATE 服务的运行以及交互依赖 Ingress 和 Ingress Controllers,如有需要,您可以选择让 FedLCM 在集群中安装一个基本的 [Ingress-NGINX Controller](https://kubernetes.github.io/ingress-nginx/) 。 + +点击 `提交` 并等待服务端点的状态变为“准备就绪”。我们可以在服务端点详情页面的“事件”栏中查看日志信息。用户可以继续在其他 K8s 集群上安装 KubeFATE 。 + +> KubeFATE 服务只能操作其本身所在的 K8s 集群,因此所有集群都应独立安装自己的 KubeFATE 服务作为服务端点。 + +## 创建联邦 + +在联邦页面点击 `新建` 按钮创建新的联邦: + +
+ +
+ +“域”一栏所填域名将会作为主域名,用于生成该联邦的 FATE 和 Exchange 各组件的域名。例如,Pulsar 服务会使用`.<主域名>` 作为域名,并使用对应的证书,各服务的 Ingress 地址也会基于此域名生成。示例中我们使用 `example.com` 作为各个服务的主域名。 + +## 创建参与方 + +### 创建 Exchange + +在 FATE 联邦详情页面的 Exchange 一栏点击 `新建` 按钮,按照提示步骤创建 Exchange。 + +> 注意: +> +> * 若要使用 FedLCM 的 Site Portal 服务,需在 chart 一栏选择“chart for FATE exchange v1.6.1 with fml-manager service”。 +> * 如需自定义证书,则在选择证书一栏选择手动安装,否则勾选“为我安装证书”来使用内置证书签发服务。 +> * 如果集群不支持负载均衡则需要选择“节点端口”服务类型。 +> * 如果集群没有开启[容器安全策略](https://kubernetes.io/docs/concepts/security/pod-security-policy/) ,则无需在相应配置项中启用。 + +最后,生成 YAML 文件并检查其内容,点击 `提交` 并等待其创建完成: + +
+ +
+ +### 创建 Cluster + +待 exchange 的状态变为“活动中”即可创建新的 cluster。点击 Cluster 一栏的 `新建` 按钮,根据提示完成配置步骤: + +
+ +
+ +> * 多数配置与上述 Exchange 的配置过程相似,详情参见以上介绍。 +> * 如需使用 site-portal 服务,则在 Chart 一栏选择"chart for FATE cluster v1.6.1 with site-portal"。 + +最后,生成 YAML 文件并检查,点击 `提交` 保存设置。 + +创建 cluster 所需时间可能比创建 exchange 更长,可以在 cluster 详情页点击事件标签查看详细日志记录。 + +## 运行 FATE Job + +此处创建了两个名为 `cluster-01` 和 `cluster-02` 的 cluster,分别对应组织 id `9999` 和 `10000`. + +
+ +
+ +点击某一 cluster 的名称进入详情页: + +
+ +
+ +如果 FATE cluster 包含 Site Portal 服务, 在公开服务一栏会有相应链接,点击进入后即可根据[文档](./Site_Portal_In_FedLCM_Configuration_Guide_zh.md)进行相关操作。 + +在 Ingress 信息一栏可以看到多个其他服务的访问地址信息,其中 client 服务提供创建 FATE job的功能,fateboard 服务提供监控 job 的功能。为了能访问到这两个服务,需要用户修改 hosts 文件或者配置 DNS 服务器,将“主机”列的域名解析到Ingress Controller服务暴露的IP地址。一般来说,这个IP地址就是“地址”列里的IP。 + +**但是如果 Ingress Controller 服务类型是“NodePort”,则需要使用相应节点的 IP 地址,而非“地址”列所示 IP。** 以下示例展示了这种使用“NodePort”类型的服务时的配置过程: + +``` +# 连接到 K8s 集群并获取ingress controller 的信息: +kubectl get svc -n ingress-nginx ingress-nginx-controller + + +# 在输出内容中查看开放的端口: +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +ingress-nginx-controller NodePort 10.103.193.113 80:30637/TCP,443:32069/TCP 119m + +# 在相应 node 上手动转发 80 端口内容至上述开放端口(替换 为开放端口号,本例中为30637): +nohup socat TCP-LISTEN:80,fork TCP:localhost: > /dev/null 2>&1 & +``` + +配置成功后即可通过 Ingress 信息栏中 client 的地址访问 FATE notebook 页面并发起FATE任务,比如[运行KubeFATE中的示例 job](https://github.com/FederatedAI/KubeFATE/blob/master/docs/tutorials/Build_Two_Parties_FATE_Cluster_in_One_Linux_Machine_with_MiniKube.md#run-a-fate-example): + +
+ +
+ +访问 fateboard 的地址查看当前运行中的 job 信息: + +
+ +
+ +进度条达到 100% 即说明联邦学习任务运行正常,在 Job Summary 界面可以查看更详细的信息: + +
+ +
+ +> 如果部署版本是 FATE v1.8.0,则也可以使用 `flow test toy` 命令快速执行联邦学习测试。 + +## 删除联邦 + +删除联邦之前,需要先删除该联邦的所有 cluster 以及 exchange,然后点击 `删除` 即可删除该联邦。 + +删除基础设施之前需要将该集群上所有服务端点删除(在此之前,该服务端点管理的 FATE cluster等也需要被移除)。 + +## 进阶使用 + +1. 上述示例仅为 FedLCM 的最简应用,FedLCM提供包括引入外部FATE连接信息,测试基础设施和服务端点等等其他功能,欢迎提供反馈。 +2. 在部署FATE时,您可以修改默认生成的 YAML 文件来满足个性化需求。 +3. 若部署了带有 Site Portal 的 FATE v1.6.1,我们可以通过这个服务创建和管理 FATE 任务。详细步骤参见[相关文档](./Site_Portal_In_FedLCM_Configuration_Guide_zh.md). diff --git a/doc/OpenFL_Guide.md b/doc/OpenFL_Guide.md new file mode 100644 index 00000000..c8584dd8 --- /dev/null +++ b/doc/OpenFL_Guide.md @@ -0,0 +1,378 @@ +# Managing OpenFL Federations + +This document provides an end-to-end guide to set up an OpenFL federation using FedLCM service. Currently, director-based mode is supported. +The overall deployment architecture is in below diagram: + +
+ +
+ +The high-level steps are + +1. FedLCM deploys the KubeFATE service into a central K8s cluster and use this KubeFATE service to deploy OpenFL director component, which includes a director service and a Jupyter Lab service. +2. On each device/node/machine that will do the actual FML training using their local data, a device-agent program is launched to register this device/node/machine to FedLCM. The registration information contains a KubeConfig file so the FedLCM can further operate the K8s cluster on the device/node/machine. +3. FedLCM deploys the KubeFATE service and the OpenFL envoy components onto the device/node/machine's K8s cluster. +4. the envoy is configured with the address of the director service, so it will register to the director service upon started. + +## Prerequisite + +* Basic understanding of the [director-based](https://openfl.readthedocs.io/en/latest/running_the_federation.html#director-based-workflow) workflow of the OpenFL project. + * Especially concepts like `shard descriptor`, `expermient` and its API, etc. +* Kubernetes clusters should be created for both the director and each device/node/machine. + * In a typical setup, the K8s cluster for running director should be a cluster in a central place like a datacenter, cloud environment, etc. + * The K8s cluster for running envoy can be a lightweight instance like K3s, in each device/node/machine. + +## Running FedLCM Service + +### Deployment +The FedLCM service can be deployed via docker-compose or in a K8s cluster. Refer to the [README](../README.md) doc for the steps. + +After deploying the FedLCM, access the web UI from a browser: +
+ +
+ +The login credential can be configured via modifying the docker-compose yaml or the k8s_deploy yaml. The default is Admin:admin. + +### Configure the CA service + +The FedLCM service depends on a CA service to issue certificates for the deployed components. To configure one, go to the Certificate section and click "NEW" button. +Currently, this service can work with a StepCA server. And both the docker-compose deployment and the K8s deployment contains a built-in StepCA server that can be used directly. + +
+ +
+ +Click "SUBMIT" to save the CA configuration. + +## Deploy Director + +With the service running and CA configured, we can start create the OpenFL federation. This includes the following steps: + +1. Add the information of the Kubernetes cluster where the director will run on to the FedLCM. +2. Create a KubeFATE endpoint service in the target Kubernetes cluster. +3. Create OpenFL federation entity. +4. Create OpenFL director. + +### Add Kubernetes Infrastructure + +Kubernetes clusters are considered as Infrastructures in the FedLCM service. All the other installation are performed on these K8s clusters. To deploy the director, one must firstly add the target K8s into the system. +Go to the "Infrastructure" section and click the "NEW" button. What needs to be filled is the KubeConfig content that FedLCM will use to connect the K8s cluster. + +**The user configured in the KubeConfig should have the privilege to create all core K8s resource including namespace, deployment, configmap, role, secret, etc. We haven't tested the exact rules. If not sure, use the cluster-admin ClusterRole** + +
+ +
+ +Click "TEST" to make sure the cluster is available. And "SUBMIT" to save the new infrastructure. + +The "FATE Registry Configuration" section is for FATE usage and not for OpenFL, we can omit that here. + +### Install KubeFATE Endpoint + +In the "Endpoint" section, we can install KubeFATE service onto the K8s infrastructure. And later it can be used to deploy OpenFL components. +To add a new KubeFATE endpoint, select the infrastructure and the system will try to find if there is already a KubeFATE service running. +If yes, the system will add the KubeFATE into its database directly. If no, the system will provide an installation step as shown below: + +
+ +
+ +Click "GET KUBEFATE INSTALLATION YAML" to get the deployment yaml. We depend on Ingress and ingress controllers to work with the KubeFATE service. +If in the Kubernetes cluster there is no ingress controller installed, we can select "Install an Ingress Controller for me" to ask FedLCM to install one. + +
+ +
+ +You can make changes to the yaml content to further customized your KubeFATE installation, but the default one is typically sufficient. + +### Create OpenFL federation + +Now, in the "Federation" page, we can create federations. The federation type is OpenFL. And we can further provide our envoy configurations and shard descriptor python source files if we enable "Customize shard descriptor". + +The below screenshot uses a customized setting based on the official [Tensorflow_MNIST](https://github.com/intel/openfl/tree/develop/openfl-tutorials/interactive_api/Tensorflow_MNIST) sample. + +
+ +
+ +In OpenFL's **ORIGINAL** design, the configured shard descriptor is used by envoy for reading and formatting the local data. And when trying to start a new training using the [Experiment API](https://openfl.readthedocs.io/en/latest/running_the_federation.html#experiment-api), users need to implement a `DataInterface` to do batching and augmenting of the data. + +This design implies that envoys are ***bounded*** to the logic in shard descriptor, which works with specific dataset. If we want to work with different dataset or format the data into different shape, we need to re-configure the director (with new shape info), re-write the shard descriptor, re-configure the envoy and restart them. + + +#### Introducing the Unbounded Shard Descriptor for Unbounded Envoy + +In FedLCM, by default, "Customize shard descriptor" is not enabled, which provides a more flexible workflow in which envoys are ***Unbounded***. + +In such setting, the sample shape and target shape will be set to `['1']`. And envoys in this federation will be configured with below config: + +```yaml +shard_descriptor: + template: dummy_shard_descriptor.FedLCMDummyShardDescriptor` +``` + +and a shard descriptor python file named `dummy_shard_descriptor.py` +```python +from typing import Iterable +from typing import List +import logging + +from openfl.interface.interactive_api.shard_descriptor import DummyShardDescriptor + +logger = logging.getLogger(__name__) + +class FedLCMDummyShardDescriptor(DummyShardDescriptor): + """Dummy shard descriptor class.""" + + def __init__(self) -> None: + """Initialize DummyShardDescriptor.""" + super().__init__(['1'], ['1'], 128) + + @property + def sample_shape(self) -> List[str]: + """Return the sample shape info.""" + return ['1'] + + @property + def target_shape(self) -> List[str]: + """Return the target shape info.""" + return ['1'] + + @property + def dataset_description(self) -> str: + logger.info( + f'Sample shape: {self.sample_shape}, ' + f'target shape: {self.target_shape}' + ) + """Return the dataset description.""" + return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data ' + 'loader to load your local data for each Envoy.' +``` + +Comparing to other specific shard descriptors, this "dummy" one doesn't contain any data reading logic. Instead, the user need to write the whole local data retrieval logic in the `DataInterface`. +This gives the flexibility to use different dataset or different data formatting logic in different experiments, without the need to re-create the director and envoy. + +### New Director + +After the federation is created, we can create the director. Click "NEW" under the "Server (Director)" section. And follow the steps in the new page. + +
+ +
+ +Several things to note: +* Try to give a unique name and namespace for the director as it may cause some issue if the name and namespace conflicts with existing ones in the cluster. +* It is suggested to choose "Install certificates for me" in the Certificate section. Only select "I will manually install certificates" if you want to import your own certificate instead of using the CA to create a new one. Refer to the OpenFL helm chart guide on how to import existing one. +* Choose "NodePort" if your cluster doesn't have any controller that can handle `LoadBalancer` type of service. +* If your cluster doesn't enable [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy/), you don't have to enable it in the "Pod Security Policy Configuration". + +Finally, get the yaml content, verify it is correct and click "SUBMIT". Now, the FedLCM system will start installing the director, which is listed in the corresponding section: + +
+ +
+ +You can click into the director details page and keep refreshing it. If things went normal, its status will be "Active" later. + +
+ +
+ +Now, we can open up the deployed Jupyter Lab system by clicking the Jupyter Notebook link, input the password we just configured and open a notebook we want to use, or create a new notebook where we can write our own code. + +* For this example we use the `interactive_api/Tensorflow_MNIST/workspace/Tensorflow_MNIST.ipynb` notebook. +* If the federation is configured with the default Unbounded Shard Descriptor, you can use `interactive_api/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb` as an example on how to put real data reading logic in the `DataInterface`. + +The content in the notebook is from OpenFL's [official repo](https://github.com/intel/openfl/tree/develop/openfl-tutorials). We assume you have basic knowledge on how to use the OpenFL sdk to work with the director API. + +For the `federation` creation part, most of the examples are using below code: + +```python +# Create a federation +from openfl.interface.interactive_api.federation import Federation + +# please use the same identificator that was used in signed certificate +client_id = 'api' +cert_dir = 'cert' +director_node_fqdn = 'localhost' +director_port=50051 +# 1) Run with API layer - Director mTLS +# If the user wants to enable mTLS their must provide CA root chain, and signed key pair to the federation interface +# cert_chain = f'{cert_dir}/root_ca.crt' +# api_certificate = f'{cert_dir}/{client_id}.crt' +# api_private_key = f'{cert_dir}/{client_id}.key' + +# federation = Federation( +# client_id=client_id, +# director_node_fqdn=director_node_fqdn, +# director_port=director_port, +# cert_chain=cert_chain, +# api_cert=api_certificate, +# api_private_key=api_private_key +# ) + +# -------------------------------------------------------------------------------------------------------------------- + +# 2) Run with TLS disabled (trusted environment) +# Federation can also determine local fqdn automatically +federation = Federation( + client_id=client_id, + director_node_fqdn=director_node_fqdn, + director_port=director_port, + tls=False +) +``` + +But we actually don't need that to be this complicated, since we have internally configured the SDK to work with the deployed director by default. +So to create a federation that represent the director, use below code is sufficient: + +```python +# Create a federation +from openfl.interface.interactive_api.federation import Federation + +federation = Federation() +``` + +And if we call the federation's `get_shard_registry()` API, we will notice the returned data is an empty dict. It is expected as current there is no "client (envoy)" created yet. +We can move on now. + +## Register Device/Node/Machine to the Federation + +We've designed and implemented a token based workflow to help the deployment of the envoy on a device/node/machine. +As mentioned in the prerequisite section, a K8s cluster needs to be up and running on the device/node/machine. + +The overall workflow is: + +1. Create a registration token in the federation page. +2. Run `openfl-device-agent` program on the device/node/machine with the token and the FedLCM's address/ + +That's it! In the federation detail page we will notice an envoy is created, and eventually it will register to the director. + +### Create Registration Token + +In the "Client (Envoy)" section, switch to the "Registration Token" tab and click the "NEW" button to create a new one. +Some attributes can be configured like expiration date, limit (how many times it can be used), labels (that will be assigned to envoys using this token), etc. + +
+ +
+ +Once a token is created, we can get the token string (staring with the `rand16:` prefix) from its detailed info section. +This token string needs to be distributed to the device/node/machine for future use. + +We can create more tokens with different properties like different labels to cope with different requirements. + +### The `openfl-device-agent` Program + +To have the envoy deployed on it, the device/node/machine needs to run a command line program called `openfl-device-agent`, who will consume the generated token. + +The binary of `openfl-device-agent` can be downloaded from the release page or built directly from the source (invoke `make openfl-device-agent` in the source tree of this project). + +The program provides two subcommand `register` and `status`. The help message of these subcommands contains basic introduction. And there are more detailed related to some options: + +#### The `--envoy-config` option + +Provides the path to a file that will be used as the envoy config for this envoy, overriding the envoy config settings in the federation in FedLCM. + +#### The `--extra-config` option + +Provides the path to a file containing extra configuration for this envoy, in yaml format. The complete configurable yaml content can be: + +```yaml +name: "type: string, default: envoy-" +description: "type: string, default: " +namespace: "type: string, default: -envoy" +chartUUID: "type: string, default: " +labels: "type: map, default:, the labels for the envoy will be a merge from this field and labels of the token" +skipCommonPythonFiles: "type: bool, default: false, if true, the python shard descriptor files configured in the federation will not be used, and user will need to manually import the python files." +enablePSP: "type: bool, default: false, if true, the deployd envoy pod will have PSP associated" +registryConfig: + useRegistry: "type: bool, default false" + registry: "type: string, default , if set, the image will be /fedlcm-openfl:v0.1.0" + useRegistrySecret: "type: bool, default false" + registrySecretConfig: + serverURL: "type: string, the address of the registry, for example, https://demo.goharbor.io" + username: "type: string" + password: "type: string" +``` + +### Start Registration and Envoy Deployment + +Assume on the device/node/machine we have the `openfl-device-agent` program, we can start the registration process + +``` +openfl-device-agent register -s 10.185.6.37 -p 9080 -t rand16:4MwELPk12Q3ybz9J -w +``` + +The `-w` option lets the program wait until the registration is finished. Alternatively, we can use the `status` subcommand to check the status of the registration process. +We can add more options to customize the envoy, as described in previous section. + +If everything goes well, the output would look like: + +``` +INFO[0000] New Envoy is being prepared, UUID: 37ae2ac4-321f-4fab-8892-6ff4339d0c18 +INFO[0000] Waiting for the preparation to finish +{"level":"info","time":"2022-05-20T05:46:20Z","message":"start for-loop with timeout 1h0m0s, interval 2s"} +INFO[0000] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0002] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0004] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0006] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0008] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0010] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0012] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0014] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0016] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0018] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0020] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0022] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0024] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0026] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0028] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Endpoint +INFO[0030] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0032] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0034] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0036] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0038] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0040] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Installing Envoy +INFO[0042] Envoy envoy-10-185-5-155(37ae2ac4-321f-4fab-8892-6ff4339d0c18) status is: Active +INFO[0042] Preparation finished +``` + +And in the federation page in the FedLCM, we will notice that an envoy is listed: + +
+ +
+ +### Add More Envoys + +If we have more devices/machines that we want to join into the federation, perform same actions as above. In this example, we added another device to this federation: +
+ +
+ +## Run an OpenFL Training Experiment + +Switch back to the Jupyter notebook page. If we call the `federation.get_shard_registry()` API again, we will notice now there are envoys registered with their shard descriptor description. +And we can continue the next steps in the notebook, which as described in all the [OpenFL official tutorial](https://openfl.readthedocs.io/en/latest/running_the_federation.html#experiment-api), include defining the `TaskInterface`, `DataInterface` and `ModelInterface`. + +If we have more devices/machines that we want to join into the federation, perform same actions as above. In this example, we added another device to this federation: +
+ +
+ +After calling the `FLExperiment.start()` API, the experiment will be handled by the director and send to all the envoys to start the training. Once it is finished, we can use the `FLExperiment.get_best_model()` and `FLExperiment.get_last_model()` API to get the trained model. +
+ +
+ + +Now, we have finished the whole process of installing FedLCM to deploying OpenFL federation to complete a training experiment! + +## Caveats +* If there are errors when running experiment in the envoy side, the experiment may become "never finished". This is OpenFL's own issue. Currently, the workaround is restart the director and envoy. +* There is no "unregister" support in OpenFL yet so if we delete an envoy, it may still show in the director's `federation.get_shard_registry()` API. But its status is offline so director won't send future experiment to this removed envoy. +* For the KubeConfig used in the infrastructure, We haven't tested what are exactly the minimal requirement permissions. \ No newline at end of file diff --git a/doc/Site_Portal_Create_Job_Guide_zh.md b/doc/Site_Portal_Create_Job_Guide_zh.md new file mode 100644 index 00000000..02e6382e --- /dev/null +++ b/doc/Site_Portal_Create_Job_Guide_zh.md @@ -0,0 +1,173 @@ +# 使用FedLCM的Site Portal创建FATE任务 + +目前在FedLCM的Site Portal服务中,我们支持3种不同类型的任务: +1. 建模(modeling) +2. 预测(prediction) +3. 隐私集合求交(PSI) + +## 建模 + +建模的第一步是填充一些基本信息,包括任务名称,任务的描述文字,参与方及数据和标签列名。 + +
+ +
+ +接下来是选择建模的方式和填写模型信息,所需的模型信息根据建模的方式不同也会有所不同。 + +我们提供两种建模模式,"JSON模板模式"和"交互式"。 + +### JSON模板模式 + +这种情况下,我们需要填写模型名称,算法和验证数据的比例(可选)。 + +目前算法支持: +1. Homo Logistic Regression +2. Homo SecureBoost + +填写完成后,点击"GENERATE CONFIGURATION", Site Portal后端会根据所选的算法和验证数据比例,基于模版生成相应的Workflow DSL文件和Algorithm Configuration文件。 + +在这一步,用户可以审阅生成的文件,并根据需求修改模版中的默认参数。 + +最后点击"SUBMIT", 此任务便可被提交到FATE-Flow开始执行。 + +
+ +
+ +### 交互式 + +在交互模式下,我们不需要填写算法和验证数据的比例,只需要填写模型名称 + +在屏幕的中央有一个画板,我们可以通过拖拽的方式将左边的组件拖拽到画板中完成建模。 + +在拖拽建模完成后,单击"GENERATE CONFIGURATION",Site Portal后段服务将根据画板中各组件的连接来生成Workflow DSL文件。 + +在画板的右侧,单击每个组件时会展示该组件相应的配置选项。单击"GENERATE CONFIGURATION"时, Site Portal后端服务将根据右边的配置生成Algorithm Configuration文件。 + +配置中有"Common"和"Difference"两种选项,当参与联邦学习的各方该组件的配置都一样时,选择"Common",否则选择"Difference"。 +修改完配置之后需要单击"Save"来进行保存。 + +
+ +
+ +下面将各个组件细述: + +#### Reader组件 +此组建必须且默认出现在画板中,它的选项已经固定为"Difference",这是因为此组件的任务是读取数据,而不同的Party应读取各自的数据。 + +#### 数据处理组件 +**DataIO/DataTransform**:用来做数据的预处理,一般接在Reader下方。在连接组件时,input表示此组件承接的组件,此例子中为"reader_0"(Site Portal前端会自动给每个组件的多个对象进行从0开始的编号)。下面举例说明如何进行连线: + +每一个组件可能有多个输出接口,每个接口输出的数据类型可能是data或model。这里"Source"表示一条连接线的开端是上一个组件的哪一个输出接口。 + +每一个组件也可能有多个输入接口,可能是data或model。这里"Target"表示一条连接线的终端指向的是当前组件的哪个输入接口。 + +
+ +
+ +关于此组件各项配置的含义,可参考[此页面](https://fate.readthedocs.io/en/develop-1.6/_build_temp/python/federatedml/util/README.html) 。 + +**HomoDataSplit**:用来分割数据集,一般接在数据预处理之后。此组件从DataIO组件获取数据,并默认将数据分成3部分: +* train data +* validate data +* test data + +
+ +
+ +通过调整此组件的配置,可以控制数据分割的比例: + +
+ +
+ +#### 特征工程组件 +特征工程组件一般接在"DataIO/DataTransform"组件下方。现阶段我们支持"HomoOneHotEncoder"组件,可以帮助将分类别的特征转化成数字化的特征以便于训练模型。具体配置可以参考[这里](https://fate.readthedocs.io/en/latest/federatedml_component/params/onehot_encoder_param/) 。 + +特征工程组件下方可以接算法组件。 + +#### 算法组件 +算法组件一般接在数据处理组件的下方,下面展示一个例子: + +
+ +
+ +这里我们拖拽了HomoLR这个算法到画板中,将HomoDataSplit分割出的训练数据接入了HomoLR组件的train_data接口;将HomeDataSplit分割出的验证数据接入了HomoLR组件的validate_data接口。 + +关于每一种算法模块的详细配置及其含义,可以参考[此页面](https://fate.readthedocs.io/en/latest/federatedml_component/#algorithm-list) 。 + +进入上述页面后,需要根据所使用FATE版本来调整页面的版本,同一算法不同版本间,算法模块的属性可能会有差异。 + +#### 模型评估组件 +模型评估组件负责将模型的评估数据返回给使用者。 +模型评估只能接在算法组件的下方,且是画板中DSL的最终节点。 + +关于模型评估组件的更多信息,可以参考[此页面](https://fate.readthedocs.io/en/latest/federatedml_component/params/evaluation_param/) 。 + +通过交互式建模,一个典型的且完整的模型将会如下图所示: + +
+ +
+ +点击"GENERATE CONFIGURATION",Site Portal后端将会根据画板中的内容生成DSL文件;根据每个组件的配置生成CONF文件。 + +用户可以审阅生成的文件内容,若没问题,单击"SUBMIT"即可将任务提交到FATE-Flow开始执行。 + +
+ +
+ +当一个建模任务执行成功时,Site Portal上的任务状态如图例所示: + +
+ +
+ +可以登录到Fate-Board上查看更多的任务信息。 + + +## 预测 + +
+ +
+ +创建一个预测任务需要以下步骤: + +1. 填写任务名 +2. 选择一个训练好的模型 +3. 选择需要被预测的数据集 +4. 提交任务 + +当一个预测任务执行成功时,Site Portal上的任务状态如图例琐所示: +œ +
+ +
+ + +## 隐私集合求交(PSI) + +隐私集合求交一般用于纵向联邦学习。例如当两家金融机构可能拥有同一批客户的不同特征时,双方想要扩展各自的特征集来达到更好的训练效果,那首先双方需要知道两边的数据集有多少共同的客户。 + +
+ +
+ +创建一个PSI任务的步骤很简单,只需要填写任务名和参加PSI任务的数据集。 + +当PSI任务跑完时,我们可以在Site Portal UI上看到结果: +1. 本方数据集有多少实例存在交集 +2. 交集占本方数据集总样本数的比例 + +
+ +
Site Portal is currently an experimental service and may include features that are in alpha or beta state. + +## Deploy +You can follow the FedLCM [getting started guide](./Getting_Started_FATE.md) to create FATE federations. **In order to use the Site Portal service, you must deploy the v1.6.1 version of FATE exchange and FATE clusters.** This means in the "Select a Chart" step when creating a new exchange and cluster, you must select the "chart for FATE exchange v1.6.1 with fml-manager service" and "chart for FATE cluster v1.6.1 with site-portal", respectively. + +In the below example, we have created a federation with 3 clusters, all containing a Site Portal service. + +
+ +
+ +Looking at the detail information page of each cluster, we can see there is a hyperlink in the "Exposed Service" section leading us to the "Site Portal" page of each site. + +
+ +
+ +## Configure Site Information +Firstly, log in with the credentials. If we didn't change such settings during the cluster deployment, the default credentials are `Admin:admin` and `User:user`. And the first thing we need to do after login is to configure this site on the "Site Configuration" page. For site 1 in this example, we input the settings as in below example: + +
+ +
+ +Several things to note: +1. The FML Manager endpoint address should be the value of the exposed FML Manager service address. And the port number should be included in the address field. We can get such information from the FedLCM's exchange details page, as the FML Manager is deployed in the exchange, as shown in the below picture. + +
+ +
+ +2. The Site Portal external address should just be the address and port of this Site Portal service, i.e., the address your browser currently is using. +3. The FATE-Flow address can be just "fateflow" and the port be "9380". You can click the "TEST" button to make sure we can connect to it. +4. We don't need to configure the Kubeflow config for now - it is a placeholder for future features. + +Before registering this Site Portal to FML Manager, we need to save the configuration. And then, we click the register button to connect to the FML Manager. Make sure we can successfully connect to it before proceeding to the next steps. A green check mark next to the FML Manager address can indicate we have finished the registration. + +For other Site Portals in other parties, we need to perform the same action for each of them and make sure they too can connect to the FML Manager. Below is the example settings for site 2. + +
+ +
+ +## Upload Local Data +CSV files can be uploaded to the system via the "Data Management" page. In this example, we use the FATE [official data](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data) to demonstrate the workflow. Site Portal needs to upload the data to the FATE cluster, so the "Upload Job Status" field of each data is the status of the "uploading to FATE" job. +> Only data in "Succeeded" status can be used in future FATE training and predicting jobs. + +We uploaded the [guest data](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_guest.csv) and [test data](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_test.csv) to site 1 and [host data](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_host.csv) to site 2, as shown below. + +
+ +
+ + +## Create Project +Once created on the "Project Management" page, this project can be viewed as a "local" project. Its information will not be sent to FML Manager, until we invite other parties to join this project. + +## Invite Other Parties +Ensure other parties' FATE cluster and Site Portal are deployed and configured, as introduced in the above sections. In the participant management tab, view other registered participants and send invitations to others. In this example, we invite site 2 from site 1. + +
+ +
+ +Then site 2 will list the invitation, and users of that party can choose to join or reject it. + +
+ +
+ +## Associate Data +All parties can associate their local data with the current project from the data management page. In this guide, both site 1 and site 2 have associated their local data with the example project. + +
+ +
+ +>When the data association changes, all participating parties should see the updated information. + +## Start Jobs +In the "job management" tab, one can create a new FATE job with other parties. We provide two modes to create FATE jobs: interactive (Drag-n-Drop) and JSON template. In this example, site 1 has created a training job to train a HomoLR model using its and site 2's data. We use the interactive mode to draw the pipeline and click the "GENERATE CONFIGURATION" button to get the DSL and Conf. Then submit it. + +
+ +
+ +The job will start after all participants have approved to start it. In this example, once site 2 approves it, the job will be submitted to FATE-Flow. We can view the job status from FATEBoard, by accessing the ingress address of it (which is listed on FedLCM's FATE cluster detail page). + +
+ +
+ +> All the joined parties can initiate new jobs. + +## Work with Trained Models +Modeling job will generate trained models that can be viewed in the "model management" tab in the project or the "model management" page on the main page. And they can be used in the "prediction" type of job. In this guide, we can start a predicting job in site 1 using the test data. + +
+ +
+ +> Currently, the publish function will return an error as FATE v1.6.1 does not support such an operation. This is a placeholder for future integration with newer FATE versions. + +> In the job detail page, we may fail to download the complete predicting result. It is a known issue in FATE-Flow and will be fixed in future releases. + +## Other Operations +* All parties can dismiss their data association so it won't be used for future jobs. +* Project participants can leave the joined project if they no longer want to participate. +* Project managing participant can close the project if it is no longer needed. +* The "User Management" page provides some configurations to set user permissions for accessing FATE Jupyter Notebook and FATEBoard. But currently it is not implemented yet. It is a placeholder for future integrations. diff --git a/doc/Site_Portal_In_FedLCM_Configuration_Guide_zh.md b/doc/Site_Portal_In_FedLCM_Configuration_Guide_zh.md new file mode 100644 index 00000000..d51da9b0 --- /dev/null +++ b/doc/Site_Portal_In_FedLCM_Configuration_Guide_zh.md @@ -0,0 +1,115 @@ +# 使用FedLCM部署的Site Portal服务 +Site Portal服务用于帮助用户以图形化的方式使用FATE,提供包括上传本地数据、创建项目并发出邀请,建立合作任务等功能。当前的FedLCM服务部署出的联邦带有Site Portal的支持,也就是说,每一个由FedLCM部署出来的FATE集群,除了带有包括FATE-Flow在内的FATE系统核心服务外,也都带有一个Site Portal服务。 + +本文档介绍了如何使用FedLCM部署的Site Portal服务,来完成FATE相关的各类任务。 + +>Site Portal服务目前还处于早期阶段,提供一些基本功能的简单实现。 + +## 部署 +我们可以根据[FedLCM的文档创建FATE的联邦](#todo),**为使用Site Portal服务,部署的FATE exchange和FATE cluster必须为v1.6.1版本**。 也就是说,在“选择Chart”的时候,我们必须选择“chart for FATE exchange v1.6.1 with fml-manager service”以及“chart for FATE cluster v1.6.1 with site-portal”这样的Chart。 + +在下图的例子中,我们创建的联邦包含3个FATE cluster,每一个cluster都包含有一个Site Portal服务。 + +
+ +
+ +当我们点击进入每一个cluster的详情页面时,会看到在“公开服务”一栏有一个“Site Portal”服务的超链接,供我们跳转到Site Portal的服务页面,从而开始后续的工作。 + +
+ +
+ +## 配置站点信息 +Site Portal已经内置有两个用户,默认用户名密码是`Admin:admin`或者`User:user`。网站的界面语言默认为英语,我们可以在登录之后,通过右上角的菜单切换语言。之后,我们需要去到“站点配置”页面去进行站点的初始化配置。在下图为我们示例环境中Site 1的站点配置: + +
+ +
+ +这里面有几点需要注意: +1. FML Manager终端地址应该是FATE exchange中暴露出来的FML Manager服务的地址,包括服务的端口号也应该写在此处。各个Site Portal服务需要通过FML Manager完成联邦任务的协调。这个地址的连接信息可以在FedLCM的exchange详情页找到。示例环境的相关信息如下图所示。 +
+ +
+ +2. Site Portal站点地址就是本站点当前的地址和端口,一般来说就是我们当前浏览器地址栏中的地址。 +3. 在FATE-Flow地址那里,我们可以写“fateflow”和“9380”即可,我们可以用“测试”按钮确认FATE-Flow工作正常。 +4. 我们目前不用配置下面的“Kubeflow配置”栏,这是为了后续功能的保留区域。 + +在点击“登记”按钮注册到FML Manager之前,我们需要先保存当前配置。之后才能正常注册到FML Manager。当一切正常时,FML Manager终端字样旁边会显示为绿色的对钩。我们只有确保注册成功,才能继续进行下面的操作。 + +对于其他站点,我们也要进行同样的配置和操作,保证各个站点都已经注册到了FML Manager。下图是示例环境中Site 2的配置。 + +
+ +
+ +## 上传本地数据 +目前Site Portal支持在数据管理页面上传本地的CSV文件。在本示例中,我们使用[FATE官方仓库中的示例数据](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data) 来演示这个过程。“上传任务状态”一栏显示的是Site Portal是否成功将数据导入到FATE系统。 +> 只有“已成功”的数据能够继续被用来参与后续的建模和预测任务。 + +我们将[guest数据](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_guest.csv) 和 [test数据](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_test.csv) 上传到Site 1之中,然后将 [host数据](https://github.com/FederatedAI/FATE/blob/v1.6.1/examples/data/breast_homo_host.csv) 上传到Site 2中。如下图所示 + +
+ +
+ +## 创建项目 +在“项目管理”页面,我们可以创建新的项目,刚刚创建的项目可以被认为是一个“本地”项目,FML Manager以及其他方还不知道这个项目的信息,只有我们向其他方发出邀请时,项目信息才会被同步过去。 + +## 邀请其他参与方 +如之前所介绍的,其他方的FATE集群的Site Portal服务需要已经正常运行,并配置和注册到了**同一个**FML Manager之中。此时,在“成员管理”页面,项目创建方就可以看到其他的可邀请的成员,并发出邀请了。在本文的例子中,我们再Site 1向Site 2发出邀请。 + +
+ +
+ +Site 2的页面中将会展示出这个邀请,它的用户可以选择接受或者拒绝这个邀请。 + +
+ +
+ +## 关联数据 +项目中的各个参与方可以将它们的本地数据关联到当前项目之中,在本文中,Site 1和Site 2之前上传成功的数据可以被关联到我们刚刚创建的项目之中。 + +
+ +
+ +>当数据关联信息发生了改变时,所有参与方都能同步到关联信息的更新。 + +## 创建任务 +在“任务管理”的子页面,我们可以创建新的FATE任务了。我们目前提供2种创建任务的方式,“交互式”或者“JSON”模板模式。在本示例中,Site 1使用自己和Site 2的数据,并通过交互式的托拉拽的方法创建了一个HomoLR的任务,如果我们了解FATE的DSL的组织形式,那么“交互式”的操作步骤并不复杂。在创建了pipeline的图形后,我们点击“生成配置文件”来获得最终的DSL和Conf内容,并点击“提交”。 + +
+ +
+ +此时任务处于“等待中”的状态,需要其他参与方接受该任务,任务才能真正被执行。在本示例中,Site 2需要接受该任务,之后任务会被提交到FATE-Flow。在提交之后,我们可以通过访问FATEBoard的ingress地址来登录FATEBoard,并查看任务的详细状态和日志。FATEBoard的Ingress地址同样在FedLCM的cluster详情页中有展示。 + +
+ +
+ +> 项目中的所有参与方都可以作为发起方发起新的任务。 + +关于创建任务的更加详细的步骤,我们有单独的[文档](/doc/Site_Portal_Create_Job_Guide_zh.md)进行更详细的展示。 + +## 使用训练好的模型 +训练任务将会产生训练好的模型,并展示在项目的“模型管理”子页面中以及“模型管理”一级页面中。而预测任务可以使用这些训练好的模型。在下图中,我们再Site 1这边使用训练好的模型来对test数据进行预测。 + +
+ +
+ +> 目前,“发布”模型功能还没有完全支持。 + +> 在预测任务的详情页面,我们可能还不能下载完整的预测结果,这是FATE-Flow本身与HDFS结合时的问题导致的,后续这个问题将会得到修复。 + +## 其他操作 +* 项目的各个参与方可以取消自己的数据关联,取消之后,各方将无法使用已经取消关联的数据创建新的任务。 +* 如果不想继续参与已经加入的某个项目,项目的各个参与方也可以选择离开该项目。 +* 项目的发起方可以直接关闭项目,所有参与方都将无法继续在项目中进行进一步的操作。 +* “用户管理”页面提供一些简单的用户权限管理界面,分别用户控制用户是否能访问Jupyter Notebook以及FATEBoard等服务,不过目前该功能还未完全开放。 diff --git a/doc/images/fate-cluster-detail.png b/doc/images/fate-cluster-detail.png new file mode 100644 index 00000000..96536501 Binary files /dev/null and b/doc/images/fate-cluster-detail.png differ diff --git a/doc/images/fate-cluster-detail_zh.png b/doc/images/fate-cluster-detail_zh.png new file mode 100644 index 00000000..e0aad746 Binary files /dev/null and b/doc/images/fate-cluster-detail_zh.png differ diff --git a/doc/images/fate-fateboard-job-detail.png b/doc/images/fate-fateboard-job-detail.png new file mode 100644 index 00000000..d4cda711 Binary files /dev/null and b/doc/images/fate-fateboard-job-detail.png differ diff --git a/doc/images/fate-fateboard-job-progress.png b/doc/images/fate-fateboard-job-progress.png new file mode 100644 index 00000000..4d76afc9 Binary files /dev/null and b/doc/images/fate-fateboard-job-progress.png differ diff --git a/doc/images/fate-federation-arch.jpg b/doc/images/fate-federation-arch.jpg new file mode 100644 index 00000000..0f481a06 Binary files /dev/null and b/doc/images/fate-federation-arch.jpg differ diff --git a/doc/images/fate-federation-detail-cluster.png b/doc/images/fate-federation-detail-cluster.png new file mode 100644 index 00000000..ff5da508 Binary files /dev/null and b/doc/images/fate-federation-detail-cluster.png differ diff --git a/doc/images/fate-federation-detail-cluster_zh.png b/doc/images/fate-federation-detail-cluster_zh.png new file mode 100644 index 00000000..d89e3e13 Binary files /dev/null and b/doc/images/fate-federation-detail-cluster_zh.png differ diff --git a/doc/images/fate-federation-detail-exchange.png b/doc/images/fate-federation-detail-exchange.png new file mode 100644 index 00000000..04062dd4 Binary files /dev/null and b/doc/images/fate-federation-detail-exchange.png differ diff --git a/doc/images/fate-federation-detail-exchange_zh.png b/doc/images/fate-federation-detail-exchange_zh.png new file mode 100644 index 00000000..0d64c79b Binary files /dev/null and b/doc/images/fate-federation-detail-exchange_zh.png differ diff --git a/doc/images/fate-new-ca.png b/doc/images/fate-new-ca.png new file mode 100644 index 00000000..6c3f9e03 Binary files /dev/null and b/doc/images/fate-new-ca.png differ diff --git a/doc/images/fate-new-ca_zh.png b/doc/images/fate-new-ca_zh.png new file mode 100644 index 00000000..061cba10 Binary files /dev/null and b/doc/images/fate-new-ca_zh.png differ diff --git a/doc/images/fate-new-cluster.png b/doc/images/fate-new-cluster.png new file mode 100644 index 00000000..cf20bbd4 Binary files /dev/null and b/doc/images/fate-new-cluster.png differ diff --git a/doc/images/fate-new-cluster_zh.png b/doc/images/fate-new-cluster_zh.png new file mode 100644 index 00000000..002eab8b Binary files /dev/null and b/doc/images/fate-new-cluster_zh.png differ diff --git a/doc/images/fate-new-endpoint-ingress.png b/doc/images/fate-new-endpoint-ingress.png new file mode 100644 index 00000000..9dfce7e2 Binary files /dev/null and b/doc/images/fate-new-endpoint-ingress.png differ diff --git a/doc/images/fate-new-endpoint.png b/doc/images/fate-new-endpoint.png new file mode 100644 index 00000000..f3f6b38f Binary files /dev/null and b/doc/images/fate-new-endpoint.png differ diff --git a/doc/images/fate-new-endpoint_zh.png b/doc/images/fate-new-endpoint_zh.png new file mode 100644 index 00000000..35d26241 Binary files /dev/null and b/doc/images/fate-new-endpoint_zh.png differ diff --git a/doc/images/fate-new-exchange-submit.png b/doc/images/fate-new-exchange-submit.png new file mode 100644 index 00000000..6c827858 Binary files /dev/null and b/doc/images/fate-new-exchange-submit.png differ diff --git a/doc/images/fate-new-exchange.png b/doc/images/fate-new-exchange.png new file mode 100644 index 00000000..c0b874a7 Binary files /dev/null and b/doc/images/fate-new-exchange.png differ diff --git a/doc/images/fate-new-federation.png b/doc/images/fate-new-federation.png new file mode 100644 index 00000000..c659c7f8 Binary files /dev/null and b/doc/images/fate-new-federation.png differ diff --git a/doc/images/fate-new-federation_zh.png b/doc/images/fate-new-federation_zh.png new file mode 100644 index 00000000..a92abb19 Binary files /dev/null and b/doc/images/fate-new-federation_zh.png differ diff --git a/doc/images/fate-new-infrastructure.png b/doc/images/fate-new-infrastructure.png new file mode 100644 index 00000000..766fed04 Binary files /dev/null and b/doc/images/fate-new-infrastructure.png differ diff --git a/doc/images/fate-new-infrastructure_zh.png b/doc/images/fate-new-infrastructure_zh.png new file mode 100644 index 00000000..d48fe3e8 Binary files /dev/null and b/doc/images/fate-new-infrastructure_zh.png differ diff --git a/doc/images/fate-notebook-job.png b/doc/images/fate-notebook-job.png new file mode 100644 index 00000000..5c4e9f31 Binary files /dev/null and b/doc/images/fate-notebook-job.png differ diff --git a/doc/images/fedlcm-login.jpg b/doc/images/fedlcm-login.jpg new file mode 100644 index 00000000..c276cd7a Binary files /dev/null and b/doc/images/fedlcm-login.jpg differ diff --git a/doc/images/fedlcm-new-ca.jpg b/doc/images/fedlcm-new-ca.jpg new file mode 100644 index 00000000..6fb4cb9c Binary files /dev/null and b/doc/images/fedlcm-new-ca.jpg differ diff --git a/doc/images/fedlcm-new-infra.jpg b/doc/images/fedlcm-new-infra.jpg new file mode 100644 index 00000000..023fc590 Binary files /dev/null and b/doc/images/fedlcm-new-infra.jpg differ diff --git a/doc/images/fedlcm-new-ingress-controller.jpg b/doc/images/fedlcm-new-ingress-controller.jpg new file mode 100644 index 00000000..b05b4034 Binary files /dev/null and b/doc/images/fedlcm-new-ingress-controller.jpg differ diff --git a/doc/images/fedlcm-new-kubefate.jpg b/doc/images/fedlcm-new-kubefate.jpg new file mode 100644 index 00000000..2aca6ee0 Binary files /dev/null and b/doc/images/fedlcm-new-kubefate.jpg differ diff --git a/doc/images/openfl-director-detail.jpg b/doc/images/openfl-director-detail.jpg new file mode 100644 index 00000000..05893083 Binary files /dev/null and b/doc/images/openfl-director-detail.jpg differ diff --git a/doc/images/openfl-federation-arch.jpg b/doc/images/openfl-federation-arch.jpg new file mode 100644 index 00000000..e33675d5 Binary files /dev/null and b/doc/images/openfl-federation-arch.jpg differ diff --git a/doc/images/openfl-new-director-installing.jpg b/doc/images/openfl-new-director-installing.jpg new file mode 100644 index 00000000..50649714 Binary files /dev/null and b/doc/images/openfl-new-director-installing.jpg differ diff --git a/doc/images/openfl-new-director.jpg b/doc/images/openfl-new-director.jpg new file mode 100644 index 00000000..94d5501e Binary files /dev/null and b/doc/images/openfl-new-director.jpg differ diff --git a/doc/images/openfl-new-envoy-2.jpg b/doc/images/openfl-new-envoy-2.jpg new file mode 100644 index 00000000..1ac9c20e Binary files /dev/null and b/doc/images/openfl-new-envoy-2.jpg differ diff --git a/doc/images/openfl-new-envoy.jpg b/doc/images/openfl-new-envoy.jpg new file mode 100644 index 00000000..4eb04a69 Binary files /dev/null and b/doc/images/openfl-new-envoy.jpg differ diff --git a/doc/images/openfl-new-federation-mnist.jpg b/doc/images/openfl-new-federation-mnist.jpg new file mode 100644 index 00000000..a2df4464 Binary files /dev/null and b/doc/images/openfl-new-federation-mnist.jpg differ diff --git a/doc/images/openfl-new-token.jpg b/doc/images/openfl-new-token.jpg new file mode 100644 index 00000000..a09f553c Binary files /dev/null and b/doc/images/openfl-new-token.jpg differ diff --git a/doc/images/openfl-notebook-model.jpg b/doc/images/openfl-notebook-model.jpg new file mode 100644 index 00000000..f626d06e Binary files /dev/null and b/doc/images/openfl-notebook-model.jpg differ diff --git a/doc/images/openfl-notebook-shard.jpg b/doc/images/openfl-notebook-shard.jpg new file mode 100644 index 00000000..ee97656b Binary files /dev/null and b/doc/images/openfl-notebook-shard.jpg differ diff --git a/doc/images/portal-accept-invite.gif b/doc/images/portal-accept-invite.gif new file mode 100644 index 00000000..3be5a783 Binary files /dev/null and b/doc/images/portal-accept-invite.gif differ diff --git a/doc/images/portal-cluster-detail.png b/doc/images/portal-cluster-detail.png new file mode 100644 index 00000000..e3c6be30 Binary files /dev/null and b/doc/images/portal-cluster-detail.png differ diff --git a/doc/images/portal-create-job.gif b/doc/images/portal-create-job.gif new file mode 100644 index 00000000..89afedcd Binary files /dev/null and b/doc/images/portal-create-job.gif differ diff --git a/doc/images/portal-create-predicting-job.gif b/doc/images/portal-create-predicting-job.gif new file mode 100644 index 00000000..6d3861ee Binary files /dev/null and b/doc/images/portal-create-predicting-job.gif differ diff --git a/doc/images/portal-data-association.png b/doc/images/portal-data-association.png new file mode 100644 index 00000000..4b3ffcdb Binary files /dev/null and b/doc/images/portal-data-association.png differ diff --git a/doc/images/portal-data-uploaded.png b/doc/images/portal-data-uploaded.png new file mode 100644 index 00000000..9d7f3f1e Binary files /dev/null and b/doc/images/portal-data-uploaded.png differ diff --git a/doc/images/portal-exchange-detail.png b/doc/images/portal-exchange-detail.png new file mode 100644 index 00000000..b74cdcff Binary files /dev/null and b/doc/images/portal-exchange-detail.png differ diff --git a/doc/images/portal-fateboard-job.png b/doc/images/portal-fateboard-job.png new file mode 100644 index 00000000..5017f1af Binary files /dev/null and b/doc/images/portal-fateboard-job.png differ diff --git a/doc/images/portal-federation.png b/doc/images/portal-federation.png new file mode 100644 index 00000000..5257f04e Binary files /dev/null and b/doc/images/portal-federation.png differ diff --git a/doc/images/portal-invite.gif b/doc/images/portal-invite.gif new file mode 100644 index 00000000..c5adefcb Binary files /dev/null and b/doc/images/portal-invite.gif differ diff --git a/doc/images/portal-modeling-basic-info.png b/doc/images/portal-modeling-basic-info.png new file mode 100644 index 00000000..4d5fda81 Binary files /dev/null and b/doc/images/portal-modeling-basic-info.png differ diff --git a/doc/images/portal-modeling-dataio.png b/doc/images/portal-modeling-dataio.png new file mode 100644 index 00000000..55288a06 Binary files /dev/null and b/doc/images/portal-modeling-dataio.png differ diff --git a/doc/images/portal-modeling-datasplit-conf.png b/doc/images/portal-modeling-datasplit-conf.png new file mode 100644 index 00000000..977d5fae Binary files /dev/null and b/doc/images/portal-modeling-datasplit-conf.png differ diff --git a/doc/images/portal-modeling-datasplit.png b/doc/images/portal-modeling-datasplit.png new file mode 100644 index 00000000..99ca4733 Binary files /dev/null and b/doc/images/portal-modeling-datasplit.png differ diff --git a/doc/images/portal-modeling-dsl.png b/doc/images/portal-modeling-dsl.png new file mode 100644 index 00000000..476873d8 Binary files /dev/null and b/doc/images/portal-modeling-dsl.png differ diff --git a/doc/images/portal-modeling-homolr.png b/doc/images/portal-modeling-homolr.png new file mode 100644 index 00000000..d0df4b9b Binary files /dev/null and b/doc/images/portal-modeling-homolr.png differ diff --git a/doc/images/portal-modeling-interactive.png b/doc/images/portal-modeling-interactive.png new file mode 100644 index 00000000..b51d9dcc Binary files /dev/null and b/doc/images/portal-modeling-interactive.png differ diff --git a/doc/images/portal-modeling-json-mode.png b/doc/images/portal-modeling-json-mode.png new file mode 100644 index 00000000..330412ae Binary files /dev/null and b/doc/images/portal-modeling-json-mode.png differ diff --git a/doc/images/portal-modeling-submit.png b/doc/images/portal-modeling-submit.png new file mode 100644 index 00000000..881e79d1 Binary files /dev/null and b/doc/images/portal-modeling-submit.png differ diff --git a/doc/images/portal-modeling-succeed.png b/doc/images/portal-modeling-succeed.png new file mode 100644 index 00000000..44cd4827 Binary files /dev/null and b/doc/images/portal-modeling-succeed.png differ diff --git a/doc/images/portal-prediction-succeed.png b/doc/images/portal-prediction-succeed.png new file mode 100644 index 00000000..7269c9af Binary files /dev/null and b/doc/images/portal-prediction-succeed.png differ diff --git a/doc/images/portal-prediction.png b/doc/images/portal-prediction.png new file mode 100644 index 00000000..2817ad64 Binary files /dev/null and b/doc/images/portal-prediction.png differ diff --git a/doc/images/portal-psi-result.png b/doc/images/portal-psi-result.png new file mode 100644 index 00000000..bf2ccc61 Binary files /dev/null and b/doc/images/portal-psi-result.png differ diff --git a/doc/images/portal-psi.png b/doc/images/portal-psi.png new file mode 100644 index 00000000..97428fb0 Binary files /dev/null and b/doc/images/portal-psi.png differ diff --git a/doc/images/portal-site-configuration-2.png b/doc/images/portal-site-configuration-2.png new file mode 100644 index 00000000..9901b9f0 Binary files /dev/null and b/doc/images/portal-site-configuration-2.png differ diff --git a/doc/images/portal-site-configuration.png b/doc/images/portal-site-configuration.png new file mode 100644 index 00000000..995257f5 Binary files /dev/null and b/doc/images/portal-site-configuration.png differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..0b0d7af0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: '3.5' + +services: + server: + image: "${SERVER_IMG}" + user: root + volumes: + - ./output/stepca:/home/step + depends_on: + - postgres + - stepca + links: + - stepca + restart: always + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: lifecycle_manager + POSTGRES_PASSWORD: lifecycle_manager + POSTGRES_DB: lifecycle_manager + LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD: admin + LIFECYCLEMANAGER_SECRETKEY: passphrase123456 + LIFECYCLEMANAGER_DEBUG: 'false' + LIFECYCLEMANAGER_BUILTINCA_HOST: stepca + LIFECYCLEMANAGER_BUILTINCA_PROVISIONER_NAME: stepca + LIFECYCLEMANAGER_BUILTINCA_PROVISIONER_PASSWORD: stepca + LIFECYCLEMANAGER_BUILTINCA_DATADIR: ./home/step + + postgres: + image: postgres:13.3 + volumes: + - ./output/data/postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_PASSWORD: lifecycle_manager + POSTGRES_USER: lifecycle_manager + POSTGRES_DB: lifecycle_manager + + frontend: + image: "${FRONTEND_IMG}" + restart: always + depends_on: + - server + ports: + - "9080:8080" + + stepca: + image: smallstep/step-ca:0.18.2 + user: root + volumes: + - ./output/stepca:/home/step + - ./make/stepca/entrypoint.sh:/entrypoint.sh + restart: always + environment: + DOCKER_STEPCA_INIT_NAME: stepca + DOCKER_STEPCA_INIT_DNS_NAMES: stepca + DOCKER_STEPCA_INIT_PASSWORD: stepca + DOCKER_STEPCA_INIT_PROVISIONER_NAME: stepca diff --git a/fml-manager/.env b/fml-manager/.env new file mode 100644 index 00000000..904546db --- /dev/null +++ b/fml-manager/.env @@ -0,0 +1,4 @@ +TAG=v0.1.0 + +SERVER_NAME=federatedai/fml-manager-server +SERVER_IMG=${SERVER_NAME}:${TAG} \ No newline at end of file diff --git a/fml-manager/.gitignore b/fml-manager/.gitignore new file mode 100644 index 00000000..1aaf8f84 --- /dev/null +++ b/fml-manager/.gitignore @@ -0,0 +1,14 @@ +*.cfg +*.idea/ +*.vscode/ +*output/ +*.todo +*.exe +dist/ +*.egg-info +*.test +.DS_Store +*.out +*.tgz +*.tar +tls/cert diff --git a/fml-manager/Makefile b/fml-manager/Makefile new file mode 100644 index 00000000..84236e31 --- /dev/null +++ b/fml-manager/Makefile @@ -0,0 +1,79 @@ +.PHONY: all clean format swag swag-bin server-unittest server run + +TAG ?= v0.1.0 + +SERVER_NAME ?= federatedai/fml-manager-server +SERVER_IMG ?= ${SERVER_NAME}:${TAG} + +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +OUTPUT_DIR = output +ifeq ($(OS),Windows_NT) +BUILD_MODE = -buildmode=exe +OUTPUT_FILE = ${OUTPUT_DIR}/fml-manager.exe +else +BUILD_MODE = +OUTPUT_FILE = ${OUTPUT_DIR}/fml-manager +endif +OUTPUT_FRONTEND_FOLDER = ${OUTPUT_DIR}/frontend + +BRANCH ?= $(shell git symbolic-ref --short HEAD) +COMMIT ?= $(shell git log --pretty=format:'%h' -n 1) +NOW ?= $(shell date "+%Y-%m-%d %T UTC%z") + +LDFLAGS = "-X 'github.com/FederatedAI/FedLCM/fml-manager/server/constants.Branch=$(BRANCH)' \ + -X 'github.com/FederatedAI/FedLCM/fml-manager/server/constants.Commit=$(COMMIT)' \ + -X 'github.com/FederatedAI/FedLCM/fml-manager/server/constants.BuildTime=$(NOW)' \ + -extldflags '-static'" + + +all: swag server + +# Run go fmt & vet against code +format: + go fmt ./... + go vet ./... + +# Build manager binary +server: format + mkdir -p ${OUTPUT_DIR} + CGO_ENABLED=0 go build -a --ldflags ${LDFLAGS} -o ${OUTPUT_FILE} ${BUILD_MODE} server/main.go + +# Run server tests +server-unittest: format + go test ./... -coverprofile cover.out + +run: format + go run --ldflags ${LDFLAGS} ./server/main.go + +# Generate swag API file +swag: swag-bin + cd server && $(SWAG_BIN) init --parseDependency --parseInternal + +swag-bin: +ifeq (, $(shell which swag)) + @{ \ + set -e ;\ + SWAG_BIN_TMP_DIR=$$(mktemp -d) ;\ + cd $$SWAG_BIN_TMP_DIR ;\ + go mod init tmp ;\ + go get -u github.com/swaggo/swag/cmd/swag ;\ + rm -rf $$SWAG_BIN_TMP_DIR ;\ + } +SWAG_BIN=$(GOBIN)/swag +else +SWAG_BIN=$(shell which swag) +endif + +docker-build: + docker build . -t ${SERVER_IMG} -f make/server/Dockerfile --build-arg BRANCH=$(BRANCH) --build-arg COMMIT=$(COMMIT) + +docker-push: + docker push ${SERVER_IMG} + +clean: + rm -rf ${OUTPUT_DIR} diff --git a/fml-manager/README.md b/fml-manager/README.md new file mode 100644 index 00000000..c10ae1c5 --- /dev/null +++ b/fml-manager/README.md @@ -0,0 +1,64 @@ +# FATE FML Manager + +A service to manage federations between different FATE sites + +## Build +``` +make all +``` +The generated deliverables are placed in the `output` folder. + +## Build & Run Docker Image +* Modify `.env` file to change the image name, and then +``` +set -a; source .env; set +a +make docker-build +``` +* Optionally push the image +``` +make docker-push +``` +* Start the service +``` +docker-compose up +``` +## Run Pre-built Images from remote registry +* Modify `.env` file to change the image names, and then +``` +docker-compose pull +docker-compose up +``` +## Enable HTTPS using docker-compose +### Create FML Manager Server and Client certificates +The server cert is used for accepting connection from site portals and the client cert is used for connecting to site portals. + +**As an example, this guide uses StepCA CLI to sign and get the certificate.** + +1. Make sure your StepCA CA server is running. Put your CA `ca.crt` into `tls/cert` folder. +Use command below to get your CA cert. +``` +step ca root +``` +2. Then `cd` to `tls/cert` folder, run commands below to create certificates and keys (replace ``(e.g. fmlmanager.fate.org) and ``(e.g. 8760h) with your configuration): +``` +step ca certificate --san localhost server.crt server.key --not-after= + +step ca certificate client.crt client.key --not-after= +``` +* For the server cert, the `localhost` SAN name is required because our sever may call itself via the localhost address. +* You can optionally add your other address and dns names as SANs in the command line. + +**If you want to use other methods to generate the certificates and keys** +* For server.crt server.key, make sure to include `` and `localhost`. + 1. If you have a usable FQDN, you can use it as your ``, and set `localhost` + 2. If you don't have a usable FQDN, set your `` with your preference. Then append with `localhost`. +* For client.crt client.key, make sure to include `` + +### Run FML Manager +* `cd` to the project root folder +``` +docker-compose -f docker-compose-https.yml up +``` + +## Deploy into Kubernetes +The are helms chart developed for installing fml-manager with the FATE exchange components together. Currently, it is used by the lifecycle-manager service. Refer to the documents in the lifecycle-manager. \ No newline at end of file diff --git a/fml-manager/docker-compose-https.yml b/fml-manager/docker-compose-https.yml new file mode 100644 index 00000000..8fedb251 --- /dev/null +++ b/fml-manager/docker-compose-https.yml @@ -0,0 +1,37 @@ +version: '3.5' + +services: + server: + image: "${SERVER_IMG}" + depends_on: + - postgres + ports: + - "8443:8443" + restart: always + volumes: + - ./tls/cert:/var/lib/fml_manager/cert + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: fml_manager + POSTGRES_PASSWORD: fml_manager + POSTGRES_DB: fml_manager + FMLMANAGER_TLS_ENABLED: 'true' + FMLMANAGER_TLS_SERVER_CERT: /var/lib/fml_manager/cert/server.crt + FMLMANAGER_TLS_SERVER_KEY: /var/lib/fml_manager/cert/server.key + FMLMANAGER_TLS_CLIENT_CERT: /var/lib/fml_manager/cert/client.crt + FMLMANAGER_TLS_CLIENT_KEY: /var/lib/fml_manager/cert/client.key + FMLMANAGER_TLS_CA_CERT: /var/lib/fml_manager/cert/ca.crt + FMLMANAGER_TLS_PORT: 8443 + + postgres: + image: postgres:13.3 + volumes: + - ./output/data/postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_PASSWORD: fml_manager + POSTGRES_USER: fml_manager + POSTGRES_DB: fml_manager \ No newline at end of file diff --git a/fml-manager/docker-compose.yml b/fml-manager/docker-compose.yml new file mode 100644 index 00000000..b91d3e08 --- /dev/null +++ b/fml-manager/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.5' + +services: + server: + image: "${SERVER_IMG}" + depends_on: + - postgres + ports: + - "8080:8080" + restart: always + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: fml_manager + POSTGRES_PASSWORD: fml_manager + POSTGRES_DB: fml_manager + FMLMANAGER_TLS_ENABLED: 'false' + + postgres: + image: postgres:13.3 + volumes: + - ./output/data/postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_PASSWORD: fml_manager + POSTGRES_USER: fml_manager + POSTGRES_DB: fml_manager \ No newline at end of file diff --git a/fml-manager/go.mod b/fml-manager/go.mod new file mode 100644 index 00000000..60d72c5b --- /dev/null +++ b/fml-manager/go.mod @@ -0,0 +1,70 @@ +module github.com/FederatedAI/FedLCM/fml-manager + +go 1.17 + +require ( + github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b + github.com/gin-contrib/logger v0.2.2 + github.com/gin-gonic/gin v1.8.1 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.27.0 + github.com/satori/go.uuid v1.2.0 + github.com/spf13/viper v1.12.0 + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe + github.com/swaggo/gin-swagger v1.5.0 + github.com/swaggo/swag v1.8.3 + gorm.io/driver/postgres v1.3.7 + gorm.io/gorm v1.23.6 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.0 // indirect + github.com/goccy/go-json v0.9.8 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.0 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.11 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fml-manager/go.sum b/fml-manager/go.sum new file mode 100644 index 00000000..6bd2f7e7 --- /dev/null +++ b/fml-manager/go.sum @@ -0,0 +1,2084 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b h1:2RoqwJw4QTJ/heCASun5F5m/kf6y86jIod/pgZ2MWYA= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b/go.mod h1:3/tZ0DwSUKl06W2exOUDLk5D9plmtwjIoIhFtVZwpTY= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim v0.9.1/go.mod h1:Y/0uV2jUab5kBI7SQgl62at0AVX7uaruzADAVmxm3eM= +github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/appleboy/gin-jwt/v2 v2.8.0/go.mod h1:KsK7E8HTvRg3vOiumTsr/ntNTHbZ3IbHLe4Eto31p7k= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.2/go.mod h1:qpbpJ1jmlqsR9f2IyaLPsdkCdnt0rbDVqIDlhuu5tRY= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= +github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= +github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc= +github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0= +github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk= +github.com/gin-contrib/logger v0.2.2 h1:xIoUvRdmfID02X09wfq7wuWmevBTdMK1T6TQjbv5r+4= +github.com/gin-contrib/logger v0.2.2/go.mod h1:6uKBteCGZF6VtxSfO1MKWl7aEu1sPSOhwCEAFPFxnnI= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE= +github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= +github.com/rubenv/sql-migrate v1.1.1/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/gin-swagger v1.4.1/go.mod h1:hmJ1vPn+XjUvnbzjCdUAxVqgraxELxk8x5zAsjCE5mg= +github.com/swaggo/gin-swagger v1.5.0 h1:hlLbxPj6qvbtX2wpbsZuOIlcnPRCUDGccA0zMKVNpME= +github.com/swaggo/gin-swagger v1.5.0/go.mod h1:3mKpZClKx7mnUGsiwJeEkNhnr1VHMkMaTAXIoFYUXrA= +github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= +github.com/swaggo/swag v1.8.0/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s= +github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.starlark.net v0.0.0-20220302181546-5411bad688d1/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220308174144-ae0e22291548/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= +gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= +gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.2/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= +gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +helm.sh/helm/v3 v3.8.0/go.mod h1:0nYPSuvuj8TTJDLRSAfbzGGbazPZsayaDpP8s9FfZT8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= +k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= +k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= +k8s.io/apiextensions-apiserver v0.23.1/go.mod h1:0qz4fPaHHsVhRApbtk3MGXNn2Q9M/cVWWhfHdY2SxiM= +k8s.io/apiextensions-apiserver v0.23.4/go.mod h1:TWYAKymJx7nLMxWCgWm2RYGXHrGlVZnxIlGnvtfYu+g= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= +k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= +k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= +k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY= +k8s.io/apiserver v0.23.4/go.mod h1:A6l/ZcNtxGfPSqbFDoxxOjEjSKBaQmE+UTveOmMkpNc= +k8s.io/cli-runtime v0.23.1/go.mod h1:r9r8H/qfXo9w+69vwUL7LokKlLRKW5D6A8vUKCx+YL0= +k8s.io/cli-runtime v0.23.4/go.mod h1:7KywUNTUibmHPqmpDFuRO1kc9RhsufHv2lkjCm2YZyM= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= +k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= +k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= +k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= +k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo= +k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E= +k8s.io/component-helpers v0.23.1/go.mod h1:ZK24U+2oXnBPcas2KolLigVVN9g5zOzaHLkHiQMFGr0= +k8s.io/component-helpers v0.23.4/go.mod h1:1Pl7L4zukZ054ElzRbvmZ1FJIU8roBXFOeRFu8zipa4= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kubectl v0.23.1/go.mod h1:Ui7dJKdUludF8yWAOSN7JZEkOuYixX5yF6E6NjoukKE= +k8s.io/kubectl v0.23.4/go.mod h1:Dgb0Rvx/8JKS/C2EuvsNiQc6RZnX0SbHJVG3XUzH6ok= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/metrics v0.23.1/go.mod h1:qXvsM1KANrc+ZZeFwj6Phvf0NLiC+d3RwcsLcdGc+xs= +k8s.io/metrics v0.23.4/go.mod h1:cl6sY9BdVT3DubbpqnkPIKi6mn/F2ltkU4yH1tEJ3Bo= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +oras.land/oras-go v1.1.0/go.mod h1:1A7vR/0KknT2UkJVWh+xMi95I/AhK8ZrxrnUSmXN0bQ= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= +sigs.k8s.io/kustomize/api v0.11.2/go.mod h1:GZuhith5YcqxIDe0GnRJNx5xxPTjlwaLTt/e+ChUtJA= +sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= +sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= +sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= +sigs.k8s.io/kustomize/kyaml v0.13.3/go.mod h1:/ya3Gk4diiQzlE4mBh7wykyLRFZNvqlbh+JnwQ9Vhrc= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/fml-manager/make/server/Dockerfile b/fml-manager/make/server/Dockerfile new file mode 100644 index 00000000..13b29872 --- /dev/null +++ b/fml-manager/make/server/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.17 as builder + +ARG BRANCH +ARG COMMIT + +WORKDIR /workspace + +COPY . . +RUN make server + +FROM photon:4.0 +WORKDIR / +COPY --from=builder /workspace/output . + +RUN tdnf install -y tzdata shadow >> /dev/null \ + && tdnf clean all \ + && groupadd -r -g 10000 manager-server \ + && useradd --no-log-init -r -m -g 10000 -u 10000 manager-server +USER manager-server + +ENTRYPOINT ["/fml-manager"] diff --git a/fml-manager/server/api/job.go b/fml-manager/server/api/job.go new file mode 100644 index 00000000..d46d435a --- /dev/null +++ b/fml-manager/server/api/job.go @@ -0,0 +1,155 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/fml-manager/server/application/service" + "github.com/FederatedAI/FedLCM/fml-manager/server/constants" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// JobController handles job related APIs +type JobController struct { + jobApp *service.JobApp +} + +// NewJobController returns a controller instance to handle job API requests +func NewJobController(jobRepo repo.JobRepository, + jobParticipantRepo repo.JobParticipantRepository, + projectRepo repo.ProjectRepository, + siteRepo repo.SiteRepository, + projectDataRepo repo.ProjectDataRepository, +) *JobController { + return &JobController{ + jobApp: &service.JobApp{ + SiteRepo: siteRepo, + JobRepo: jobRepo, + ProjectRepo: projectRepo, + ParticipantRepo: jobParticipantRepo, + ProjectDataRepo: projectDataRepo, + }, + } +} + +// Route set up route mappings to job related APIs +func (controller *JobController) Route(r *gin.RouterGroup) { + job := r.Group("job") + if viper.GetBool("fmlmanager.tls.enabled") { + job.Use(certAuthenticator()) + } + { + job.POST("/create", controller.handleJobCreation) + job.POST("/:uuid/response", controller.handleJobResponse) + job.POST("/:uuid/status", controller.handleJobStatusUpdate) + } +} + +// handleJobCreation process a job creation request +// @Summary Process job creation +// @Tags Job +// @Produce json +// @Param project body service.JobRemoteJobCreationRequest true "job creation request" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/create [post] +func (controller *JobController) handleJobCreation(c *gin.Context) { + if err := func() error { + creationRequest := &service.JobRemoteJobCreationRequest{} + if err := c.ShouldBindJSON(creationRequest); err != nil { + return err + } + return controller.jobApp.ProcessJobCreationRequest(creationRequest) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleJobResponse process a job approval response +// @Summary Process job response +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Param project body service.JobApprovalContext true "job approval response" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/response [post] +func (controller *JobController) handleJobResponse(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + context := &service.JobApprovalContext{} + if err := c.ShouldBindJSON(context); err != nil { + return err + } + return controller.jobApp.ProcessJobApprovalResponse(jobUUID, context) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleJobStatusUpdate process a job status update request +// @Summary Process job status update +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Param project body service.JobStatusUpdateContext true "job status" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/status [post] +func (controller *JobController) handleJobStatusUpdate(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + context := &service.JobStatusUpdateContext{} + if err := c.ShouldBindJSON(context); err != nil { + return err + } + return controller.jobApp.ProcessJobStatusUpdate(jobUUID, context) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/fml-manager/server/api/project.go b/fml-manager/server/api/project.go new file mode 100644 index 00000000..3097c0fa --- /dev/null +++ b/fml-manager/server/api/project.go @@ -0,0 +1,452 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/fml-manager/server/application/service" + "github.com/FederatedAI/FedLCM/fml-manager/server/constants" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/event" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// ProjectController handles project related APIs +type ProjectController struct { + projectApp *service.ProjectApp +} + +// NewProjectController returns a controller instance to handle project API requests +func NewProjectController(projectRepo repo.ProjectRepository, siteRepo repo.SiteRepository, + participantRepo repo.ProjectParticipantRepository, + invitationRepo repo.ProjectInvitationRepository, + projectDataRepo repo.ProjectDataRepository) *ProjectController { + return &ProjectController{ + projectApp: &service.ProjectApp{ + ProjectRepo: projectRepo, + SiteRepo: siteRepo, + ParticipantRepo: participantRepo, + InvitationRepo: invitationRepo, + ProjectDataRepo: projectDataRepo, + }, + } +} + +// Route set up route mappings to project related APIs +func (controller *ProjectController) Route(r *gin.RouterGroup) { + project := r.Group("project") + if viper.GetBool("fmlmanager.tls.enabled") { + project.Use(certAuthenticator()) + } + { + project.POST("/invitation", controller.handleInvitation) + project.POST("/invitation/:uuid/accept", controller.handleInvitationAcceptance) + project.POST("/invitation/:uuid/reject", controller.handleInvitationRejection) + project.POST("/invitation/:uuid/revoke", controller.handleInvitationRevocation) + + project.POST("/:uuid/participant/:siteUUID/leave", controller.handleParticipantLeaving) + project.POST("/:uuid/participant/:siteUUID/dismiss", controller.handleParticipantDismissal) + + project.POST("/event/participant/update", controller.handleParticipantInfoUpdate) + + project.POST("/:uuid/data/associate", controller.handleDataAssociation) + project.POST("/:uuid/data/dismiss", controller.handleDataDismissal) + + project.POST("/:uuid/close", controller.handleProjectClosing) + + project.GET("", controller.list) + project.GET("/:uuid/participant", controller.listParticipant) + project.GET("/:uuid/data", controller.listData) + } +} + +// handleInvitation process a project invitation +// @Summary Process project invitation +// @Tags Project +// @Produce json +// @Param project body service.ProjectInvitationRequest true "invitation request" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/invitation [post] +func (controller *ProjectController) handleInvitation(c *gin.Context) { + if err := func() error { + invitationRequest := &service.ProjectInvitationRequest{} + if err := c.ShouldBindJSON(invitationRequest); err != nil { + return err + } + return controller.projectApp.ProcessInvitation(invitationRequest) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationAcceptance process a project invitation acceptance +// @Summary Process invitation acceptance response +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/invitation/{uuid}/accept [post] +func (controller *ProjectController) handleInvitationAcceptance(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationResponse(invitationUUID, true) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationRejection process a project invitation rejection +// @Summary Process invitation rejection response +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/invitation/{uuid}/reject [post] +func (controller *ProjectController) handleInvitationRejection(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationResponse(invitationUUID, false) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationRevocation process a project invitation revocation +// @Summary Process invitation revocation request +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/invitation/{uuid}/revoke [post] +func (controller *ProjectController) handleInvitationRevocation(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationRevocation(invitationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantInfoUpdate process a participant info update event +// @Summary Process participant info update event, called by this FML manager's site context only +// @Tags Project +// @Produce json +// @Param project body event.ProjectParticipantUpdateEvent true "Updated participant info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/event/participant/update [post] +func (controller *ProjectController) handleParticipantInfoUpdate(c *gin.Context) { + if err := func() error { + updateEvent := &event.ProjectParticipantUpdateEvent{} + if err := c.ShouldBindJSON(updateEvent); err != nil { + return err + } + return controller.projectApp.ProcessParticipantInfoUpdate(updateEvent.UUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleDataAssociation process a new data association +// @Summary Process new data association from site +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param project body service.ProjectDataAssociation true "Data association info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data/associate [post] +func (controller *ProjectController) handleDataAssociation(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + association := &service.ProjectDataAssociation{} + if err := c.ShouldBindJSON(association); err != nil { + return err + } + return controller.projectApp.ProcessDataAssociation(projectUUID, association) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleDataDismissal process project data dismissal +// @Summary Process data dismissal from site +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param project body service.ProjectDataAssociationBase true "Data association info containing the data UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data/dismiss [post] +func (controller *ProjectController) handleDataDismissal(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + association := &service.ProjectDataAssociationBase{} + if err := c.ShouldBindJSON(association); err != nil { + return err + } + return controller.projectApp.ProcessDataDismissal(projectUUID, association) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantLeaving process project participant leaving +// @Summary Process participant leaving +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param siteUUID path string true "Site UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/participant/{siteUUID}/leave [post] +func (controller *ProjectController) handleParticipantLeaving(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + siteUUID := c.Param("siteUUID") + return controller.projectApp.ProcessParticipantLeaving(projectUUID, siteUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantDismissal process project participant dismissal +// @Summary Process participant dismissal, called by the managing site only +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param siteUUID path string true "Site UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/participant/{siteUUID}/dismiss [post] +func (controller *ProjectController) handleParticipantDismissal(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + siteUUID := c.Param("siteUUID") + return controller.projectApp.ProcessParticipantDismissal(projectUUID, siteUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleProjectClosing process project closing +// @Summary Process project closing +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/close [post] +func (controller *ProjectController) handleProjectClosing(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.ProcessProjectClosing(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// list returns all projects or project related to the specified participant +// @Summary List all project +// @Tags Project +// @Produce json +// @Param participant query string false "participant uuid, if set, only returns the projects containing the participant" +// @Success 200 {object} GeneralResponse{data=map[string]service.ProjectInfoWithStatus} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project [get] +func (controller *ProjectController) list(c *gin.Context) { + // TODO: use token to extract participant uuid and do authz check + participantUUID := c.DefaultQuery("participant", "") + projectList, err := controller.projectApp.ListProjectByParticipant(participantUUID) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: projectList, + } + c.JSON(http.StatusOK, resp) + } +} + +// listData returns all data association in a project +// @Summary List all data association in a project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=map[string]service.ProjectDataAssociation} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data [get] +func (controller *ProjectController) listData(c *gin.Context) { + // TODO: use token to verify the requester can access these info + projectUUID := c.Param("uuid") + dataList, err := controller.projectApp.ListDataAssociationByProject(projectUUID) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: dataList, + } + c.JSON(http.StatusOK, resp) + } +} + +// listParticipant returns all participants info in a project +// @Summary List all participants in a project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=map[string]service.ProjectDataAssociation} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/participant [get] +func (controller *ProjectController) listParticipant(c *gin.Context) { + // TODO: use token to verify the requester can access these info + projectUUID := c.Param("uuid") + participantList, err := controller.projectApp.ListParticipantByProject(projectUUID) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: participantList, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/fml-manager/server/api/site.go b/fml-manager/server/api/site.go new file mode 100644 index 00000000..8ec7d785 --- /dev/null +++ b/fml-manager/server/api/site.go @@ -0,0 +1,107 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/fml-manager/server/application/service" + "github.com/FederatedAI/FedLCM/fml-manager/server/constants" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// SiteController provides API handlers for the site related APIs +type SiteController struct { + siteAppService *service.SiteApp +} + +// NewSiteController returns a controller instance to handle site API requests +func NewSiteController(repo repo.SiteRepository) *SiteController { + return &SiteController{ + siteAppService: &service.SiteApp{ + SiteRepo: repo, + }, + } +} + +// Route set up route mappings to site related APIs +func (controller *SiteController) Route(r *gin.RouterGroup) { + site := r.Group("site") + if viper.GetBool("fmlmanager.tls.enabled") { + site.Use(certAuthenticator()) + } + { + site.GET("", controller.getSite) + site.POST("", controller.postSite) + } +} + +// getSite returns the sites list +// @Summary Return sites list +// @Tags Site +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]entity.Site} "Success" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site [get] +func (controller *SiteController) getSite(c *gin.Context) { + siteList, err := controller.siteAppService.GetSiteList() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: siteList, + } + c.JSON(http.StatusOK, resp) + } +} + +// postSite creates or updates site information +// @Summary Create or update site info +// @Tags Site +// @Produce json +// @Param site body entity.Site true "The site information" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site [post] +func (controller *SiteController) postSite(c *gin.Context) { + if err := func() error { + updatedSiteInfo := &entity.Site{} + if err := c.ShouldBindJSON(updatedSiteInfo); err != nil { + return err + } + return controller.siteAppService.RegisterSite(updatedSiteInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/fml-manager/server/api/tls_auth.go b/fml-manager/server/api/tls_auth.go new file mode 100644 index 00000000..c8ab6a31 --- /dev/null +++ b/fml-manager/server/api/tls_auth.go @@ -0,0 +1,34 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +func certAuthenticator() gin.HandlerFunc { + return func(c *gin.Context) { + + clientCert := c.Request.TLS.PeerCertificates[0] + clientCommonName := clientCert.Subject.CommonName + log.Info().Msgf("Request URL: %s", c.Request.URL.String()) + log.Info().Msgf("Client common name is: %s", clientCommonName) + + // TODO: validate the clientCommonName's domain is same to the fmlManagerCommonName's domain + + c.Next() + } +} diff --git a/fml-manager/server/api/types.go b/fml-manager/server/api/types.go new file mode 100644 index 00000000..c59fa812 --- /dev/null +++ b/fml-manager/server/api/types.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// GeneralResponse is used for typical API response +type GeneralResponse struct { + Code int `json:"code" example:"0"` + Message string `json:"message" example:"success"` + Data interface{} `json:"data" swaggertype:"object"` +} diff --git a/fml-manager/server/application/service/job_service.go b/fml-manager/server/application/service/job_service.go new file mode 100644 index 00000000..f7c0c737 --- /dev/null +++ b/fml-manager/server/application/service/job_service.go @@ -0,0 +1,307 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/service" + "github.com/pkg/errors" +) + +// JobApp provides functions to handle job related events +type JobApp struct { + SiteRepo repo.SiteRepository + JobRepo repo.JobRepository + ParticipantRepo repo.JobParticipantRepository + ProjectRepo repo.ProjectRepository + ProjectDataRepo repo.ProjectDataRepository +} + +// JobDataBase describes one data configuration for a job +type JobDataBase struct { + DataUUID string `json:"data_uuid"` + LabelName string `json:"label_name"` +} + +// JobApprovalContext contains the issuing site and the approval result +type JobApprovalContext struct { + SiteUUID string `json:"site_uuid"` + Approved bool `json:"approved"` +} + +// JobRemoteJobCreationRequest is the structure containing necessary info to create a job +type JobRemoteJobCreationRequest struct { + UUID string `json:"uuid"` + ConfJson string `json:"conf_json"` + DSLJson string `json:"dsl_json"` + Name string `json:"name"` + Description string `json:"description"` + Type entity.JobType `json:"type"` + ProjectUUID string `json:"project_uuid"` + InitiatorData JobDataBase `json:"initiator_data"` + OtherData []JobDataBase `json:"other_site_data"` + ValidationEnabled bool `json:"training_validation_enabled"` + ValidationSizePercent uint `json:"training_validation_percent"` + ModelName string `json:"training_model_name"` + AlgorithmType entity.JobAlgorithmType `json:"training_algorithm_type"` + AlgorithmComponentName string `json:"algorithm_component_name"` + EvaluateComponentName string `json:"evaluate_component_name"` + ComponentsToDeploy []string `json:"training_component_list_to_deploy"` + ModelUUID string `json:"predicting_model_uuid"` + Username string `json:"username"` +} + +// JobStatusUpdateContext contain necessary info for updating job status, including status of the participants +type JobStatusUpdateContext struct { + Status entity.JobStatus `json:"status"` + StatusMessage string `json:"status_message"` + FATEJobID string `json:"fate_job_id"` + FATEJobStatus string `json:"fate_job_status"` + FATEModelID string `json:"fate_model_id"` + FATEModelVersion string `json:"fate_model_version"` + ParticipantStatusMap map[string]entity.JobParticipantStatus `json:"participant_status_map"` +} + +// ProcessJobCreationRequest builds the creation requests and calls the domain service +func (app *JobApp) ProcessJobCreationRequest(request *JobRemoteJobCreationRequest) error { + projectDataInstance, err := app.ProjectDataRepo.GetByProjectAndDataUUID(request.ProjectUUID, request.InitiatorData.DataUUID) + if err != nil { + return errors.Wrapf(err, "failed to get initiator data") + } + projectData := projectDataInstance.(*entity.ProjectData) + + siteInstance, err := app.SiteRepo.GetByUUID(projectData.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find initiating site") + } + site := siteInstance.(*entity.Site) + + requestJsonByte, err := json.MarshalIndent(request, "", " ") + if err != nil { + return err + } + requestJsonStr := string(requestJsonByte) + + jobService := &service.JobService{ + JobRepo: app.JobRepo, + ParticipantRepo: app.ParticipantRepo, + } + + creationRequest := &service.JobCreationRequest{ + Job: &entity.Job{ + Name: request.Name, + Description: request.Description, + UUID: request.UUID, + ProjectUUID: request.ProjectUUID, + Type: request.Type, + AlgorithmType: request.AlgorithmType, + AlgorithmConfig: entity.AlgorithmConfig{ + TrainingValidationEnabled: request.ValidationEnabled, + TrainingValidationSizePercent: request.ValidationSizePercent, + TrainingComponentsToDeploy: request.ComponentsToDeploy, + }, + ModelName: request.ModelName, + PredictingModelUUID: request.ModelUUID, + InitiatingSiteUUID: site.UUID, + InitiatingSiteName: site.Name, + InitiatingSitePartyID: site.PartyID, + InitiatingUser: request.Username, + Conf: request.ConfJson, + DSL: request.DSLJson, + RequestJson: requestJsonStr, + Repo: app.JobRepo, + }, + Initiator: service.JobParticipantSiteInfo{ + JobParticipant: &entity.JobParticipant{ + JobUUID: request.UUID, + SiteUUID: site.UUID, + SiteName: site.Name, + SitePartyID: site.PartyID, + DataUUID: projectData.DataUUID, + DataName: projectData.Name, + DataDescription: projectData.Description, + DataTableName: projectData.TableName, + DataTableNamespace: projectData.TableNamespace, + DataLabelName: request.InitiatorData.LabelName, + Repo: app.ParticipantRepo, + }, + JobParticipantConnectionInfo: service.JobParticipantConnectionInfo{ + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + }, + }, + Participants: map[string]service.JobParticipantSiteInfo{}, + } + + for _, otherData := range request.OtherData { + projectDataInstance, err = app.ProjectDataRepo.GetByProjectAndDataUUID(request.ProjectUUID, otherData.DataUUID) + if err != nil { + return errors.Wrapf(err, "failed to get other site data: %s", otherData.DataUUID) + } + projectData = projectDataInstance.(*entity.ProjectData) + + siteInstance, err := app.SiteRepo.GetByUUID(projectData.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find other site") + } + otherSite := siteInstance.(*entity.Site) + + creationRequest.Participants[projectData.SiteUUID] = service.JobParticipantSiteInfo{ + JobParticipant: &entity.JobParticipant{ + JobUUID: request.UUID, + SiteUUID: otherSite.UUID, + SiteName: otherSite.Name, + SitePartyID: otherSite.PartyID, + DataUUID: projectData.DataUUID, + DataName: projectData.Name, + DataDescription: projectData.Description, + DataTableName: projectData.TableName, + DataTableNamespace: projectData.TableNamespace, + DataLabelName: request.InitiatorData.LabelName, + Repo: app.ParticipantRepo, + }, + JobParticipantConnectionInfo: service.JobParticipantConnectionInfo{ + ExternalHost: otherSite.ExternalHost, + ExternalPort: otherSite.ExternalPort, + HTTPS: otherSite.HTTPS, + ServerName: otherSite.ServerName, + }, + } + } + return jobService.HandleNewJobCreation(creationRequest) +} + +// ProcessJobApprovalResponse calls the domain service to process the job approval response +func (app *JobApp) ProcessJobApprovalResponse(jobUUID string, context *JobApprovalContext) error { + jobInstance, err := app.JobRepo.GetByUUID(jobUUID) + if err != nil { + return errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + job.Repo = app.JobRepo + + siteInstance, err := app.SiteRepo.GetByUUID(job.InitiatingSiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find initiating site") + } + initiatingSite := siteInstance.(*entity.Site) + + siteInstance, err = app.SiteRepo.GetByUUID(context.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find initiating site") + } + approvingSite := siteInstance.(*entity.Site) + + jobParticipantInstance, err := app.ParticipantRepo.GetByJobAndSiteUUID(job.UUID, approvingSite.UUID) + if err != nil { + return errors.Wrap(err, "failed to get participant info") + } + jobParticipant := jobParticipantInstance.(*entity.JobParticipant) + jobParticipant.Repo = app.ParticipantRepo + + response := &service.JobApprovalResponse{ + Initiator: service.JobParticipantConnectionInfo{ + ExternalHost: initiatingSite.ExternalHost, + ExternalPort: initiatingSite.ExternalPort, + HTTPS: initiatingSite.HTTPS, + ServerName: initiatingSite.ServerName, + }, + Participants: map[string]service.JobParticipantConnectionInfo{}, + ApprovingSite: jobParticipant, + Approved: context.Approved, + JobUUID: jobUUID, + } + + participantListInstance, err := app.ParticipantRepo.GetListByJobUUID(jobUUID) + if err != nil { + return err + } + participantList := participantListInstance.([]entity.JobParticipant) + for index := range participantList { + if participantList[index].SiteUUID != initiatingSite.UUID { + siteInstance, err := app.SiteRepo.GetByUUID(participantList[index].SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find initiating site") + } + site := siteInstance.(*entity.Site) + response.Participants[site.UUID] = service.JobParticipantConnectionInfo{ + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + } + } + jobService := &service.JobService{ + JobRepo: app.JobRepo, + ParticipantRepo: app.ParticipantRepo, + } + return jobService.HandleJobApprovalResponse(response) +} + +// ProcessJobStatusUpdate calls the domain service to handle the job status update event +func (app *JobApp) ProcessJobStatusUpdate(jobUUID string, context *JobStatusUpdateContext) error { + jobService := &service.JobService{ + JobRepo: app.JobRepo, + ParticipantRepo: app.ParticipantRepo, + } + + jobInstance, err := app.JobRepo.GetByUUID(jobUUID) + if err != nil { + return errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + job.Repo = app.JobRepo + + updateContext := &service.JobStatusUpdateContext{ + JobUUID: jobUUID, + NewJobStatus: &entity.Job{ + Status: context.Status, + StatusMessage: context.StatusMessage, + FATEJobID: context.FATEJobID, + FATEJobStatus: context.FATEJobStatus, + FATEModelID: context.FATEModelID, + FATEModelVersion: context.FATEModelVersion, + }, + ParticipantStatusMap: map[string]service.JobParticipantStatusInfo{}, + } + requestJsonByte, err := json.Marshal(context) + if err != nil { + return err + } + updateContext.RequestJson = string(requestJsonByte) + for siteUUID := range context.ParticipantStatusMap { + siteInstance, err := app.SiteRepo.GetByUUID(siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find initiating site") + } + site := siteInstance.(*entity.Site) + updateContext.ParticipantStatusMap[siteUUID] = service.JobParticipantStatusInfo{ + JobParticipantStatus: context.ParticipantStatusMap[siteUUID], + JobParticipantConnectionInfo: service.JobParticipantConnectionInfo{ + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + }, + } + } + return jobService.HandleJobStatusUpdate(updateContext) +} diff --git a/fml-manager/server/application/service/project_service.go b/fml-manager/server/application/service/project_service.go new file mode 100644 index 00000000..23056062 --- /dev/null +++ b/fml-manager/server/application/service/project_service.go @@ -0,0 +1,621 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/service" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/valueobject" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectApp provides functions to handle project related events +type ProjectApp struct { + ProjectRepo repo.ProjectRepository + ParticipantRepo repo.ProjectParticipantRepository + SiteRepo repo.SiteRepository + InvitationRepo repo.ProjectInvitationRepository + ProjectDataRepo repo.ProjectDataRepository +} + +// ProjectInvitationRequest is an invitation for asking a site to join a project +type ProjectInvitationRequest struct { + UUID string `json:"uuid"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` + AssociatedData []ProjectDataAssociation `json:"associated_data"` +} + +// ProjectDataAssociationBase contains the basic info of a project data association +type ProjectDataAssociationBase struct { + DataUUID string `json:"data_uuid"` +} + +// ProjectDataAssociation represents a data associated in a project +type ProjectDataAssociation struct { + ProjectDataAssociationBase + Name string `json:"name"` + Description string `json:"description"` + SiteName string `json:"site_name"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + TableName string `json:"table_name"` + TableNamespace string `json:"table_namespace"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ProjectInfoWithStatus contains project basic information and the status inferred for certain participant +type ProjectInfoWithStatus struct { + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` + ProjectStatus entity.ProjectStatus `json:"project_status"` +} + +// ProcessInvitation handles new invitation request +func (app *ProjectApp) ProcessInvitation(req *ProjectInvitationRequest) error { + project := &entity.Project{ + UUID: req.ProjectUUID, + Name: req.ProjectName, + Description: req.ProjectDescription, + AutoApprovalEnabled: req.ProjectAutoApprovalEnabled, + ProjectCreatorInfo: &valueobject.ProjectCreatorInfo{ + Manager: req.ProjectManager, + ManagingSiteName: req.ProjectManagingSiteName, + ManagingSitePartyID: req.ProjectManagingSitePartyID, + ManagingSiteUUID: req.ProjectManagingSiteUUID, + }, + Repo: app.ProjectRepo, + Model: gorm.Model{CreatedAt: req.ProjectCreationTime}, + } + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + invitationReq := &service.ProjectInvitationRequest{ + InvitationUUID: req.UUID, + Project: project, + ManagingSite: nil, + TargetSite: nil, + } + siteInstance, err := app.SiteRepo.GetByUUID(req.ProjectManagingSiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find managing site") + } + site := siteInstance.(*entity.Site) + invitationReq.ManagingSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + siteInstance, err = app.SiteRepo.GetByUUID(req.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find target site") + } + site = siteInstance.(*entity.Site) + invitationReq.TargetSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + projectDataList := make([]entity.ProjectData, len(req.AssociatedData)) + for index, data := range req.AssociatedData { + projectDataList[index] = entity.ProjectData{ + Name: data.Name, + Description: data.Description, + ProjectUUID: req.ProjectUUID, + DataUUID: data.DataUUID, + SiteUUID: data.SiteUUID, + SiteName: data.SiteName, + SitePartyID: data.SitePartyID, + TableName: data.TableName, + TableNamespace: data.TableNamespace, + CreationTime: data.CreationTime, + UpdateTime: data.UpdateTime, + } + } + invitationReq.AssociatedData = projectDataList + return projectService.HandleInvitationRequest(invitationReq) +} + +// ProcessInvitationResponse handles invitation response +func (app *ProjectApp) ProcessInvitationResponse(invitationUUID string, accepted bool) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + invitationInstance, err := app.InvitationRepo.GetByUUID(invitationUUID) + if err != nil { + return errors.Wrapf(err, "failed to find invitation") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + + projectInstance, err := app.ProjectRepo.GetByUUID(invitation.ProjectUUID) + if err != nil { + return errors.Wrapf(err, "failed to find project") + } + project := projectInstance.(*entity.Project) + + invitationReq := &service.ProjectInvitationRequest{ + InvitationUUID: invitation.UUID, + Project: project, + ManagingSite: nil, + TargetSite: nil, + } + + siteInstance, err := app.SiteRepo.GetByUUID(project.ManagingSiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find managing site") + } + site := siteInstance.(*entity.Site) + invitationReq.ManagingSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + siteInstance, err = app.SiteRepo.GetByUUID(invitation.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find target site") + } + site = siteInstance.(*entity.Site) + invitationReq.TargetSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + + if accepted { + participantListInstance, err := app.ParticipantRepo.GetByProjectUUID(project.UUID) + if err != nil { + return err + } + participantList := participantListInstance.([]entity.ProjectParticipant) + var otherSiteList []service.ProjectParticipantSiteInfo + for _, participant := range participantList { + if participant.Status == entity.ProjectParticipantStatusJoined && participant.SiteUUID != invitation.SiteUUID { + siteInstance, err = app.SiteRepo.GetByUUID(participant.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find target site") + } + site = siteInstance.(*entity.Site) + otherSiteList = append(otherSiteList, service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + }) + } + } + return projectService.HandleInvitationAcceptance(invitationReq, otherSiteList) + } else { + return projectService.HandleInvitationRejection(invitationReq) + } +} + +// ProcessInvitationRevocation handles invitation revocation request +func (app *ProjectApp) ProcessInvitationRevocation(invitationUUID string) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + invitationInstance, err := app.InvitationRepo.GetByUUID(invitationUUID) + if err != nil { + return errors.Wrapf(err, "failed to find invitation") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + + projectInstance, err := app.ProjectRepo.GetByUUID(invitation.ProjectUUID) + if err != nil { + return errors.Wrapf(err, "failed to find project") + } + project := projectInstance.(*entity.Project) + + invitationReq := &service.ProjectInvitationRequest{ + InvitationUUID: invitation.UUID, + Project: project, + ManagingSite: nil, + TargetSite: nil, + } + + siteInstance, err := app.SiteRepo.GetByUUID(project.ManagingSiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find managing site") + } + site := siteInstance.(*entity.Site) + invitationReq.ManagingSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + siteInstance, err = app.SiteRepo.GetByUUID(invitation.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find target site") + } + site = siteInstance.(*entity.Site) + invitationReq.TargetSite = &service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + return projectService.HandleInvitationRevocation(invitationReq) +} + +// ProcessParticipantInfoUpdate handles sites info update event +func (app *ProjectApp) ProcessParticipantInfoUpdate(siteUUID string) error { + siteInstance, err := app.SiteRepo.GetByUUID(siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find updated site") + } + updatedSite := siteInstance.(*entity.Site) + + list, err := app.SiteRepo.GetSiteList() + if err != nil { + return errors.Wrapf(err, "failed to list all sites") + } + allSites := list.([]entity.Site) + + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + allSitesInfo := make([]service.ProjectParticipantSiteInfo, len(allSites)) + for index, site := range allSites { + allSitesInfo[index] = service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + } + + return projectService.HandleParticipantInfoUpdate(service.ProjectParticipantSiteInfo{ + Name: updatedSite.Name, + Description: updatedSite.Description, + UUID: updatedSite.UUID, + PartyID: updatedSite.PartyID, + ExternalHost: updatedSite.ExternalHost, + ExternalPort: updatedSite.ExternalPort, + HTTPS: updatedSite.HTTPS, + ServerName: updatedSite.ServerName, + }, allSitesInfo) +} + +// ProcessParticipantLeaving handles participate leaving +func (app *ProjectApp) ProcessParticipantLeaving(projectUUID, siteUUID string) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + otherSiteList, err := app.getPeerParticipantList(projectUUID, siteUUID) + if err != nil { + return errors.New("failed to get peer participant list") + } + return projectService.HandleParticipantLeaving(projectUUID, siteUUID, otherSiteList) +} + +// ProcessParticipantDismissal handles participate dismissal +func (app *ProjectApp) ProcessParticipantDismissal(projectUUID, siteUUID string) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrapf(err, "failed to find project") + } + project := projectInstance.(*entity.Project) + + otherSiteList, err := app.getPeerParticipantList(projectUUID, project.ManagingSiteUUID) + if err != nil { + return errors.New("failed to get peer participant list") + } + + siteInstance, err := app.SiteRepo.GetByUUID(siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to find target site") + } + site := siteInstance.(*entity.Site) + targetSite := service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + } + return projectService.HandleParticipantDismissal(projectUUID, targetSite, otherSiteList) +} + +// ProcessDataAssociation handles new data association +func (app *ProjectApp) ProcessDataAssociation(projectUUID string, data *ProjectDataAssociation) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + otherSiteList, err := app.getPeerParticipantList(projectUUID, data.SiteUUID) + if err != nil { + return errors.New("failed to get peer participant list") + } + return projectService.HandleDataAssociation(&entity.ProjectData{ + Name: data.Name, + Description: data.Description, + ProjectUUID: projectUUID, + DataUUID: data.DataUUID, + SiteUUID: data.SiteUUID, + SiteName: data.SiteName, + SitePartyID: data.SitePartyID, + TableName: data.TableName, + TableNamespace: data.TableNamespace, + CreationTime: data.CreationTime, + UpdateTime: data.UpdateTime, + }, otherSiteList) +} + +// ProcessDataDismissal handles data association dismissal +func (app *ProjectApp) ProcessDataDismissal(projectUUID string, baseData *ProjectDataAssociationBase) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + dataInstance, err := app.ProjectDataRepo.GetByProjectAndDataUUID(projectUUID, baseData.DataUUID) + if err != nil { + if err == repo.ErrProjectDataNotFound { + return nil + } + return errors.Wrap(err, "failed to query association") + } + data := dataInstance.(*entity.ProjectData) + otherSiteList, err := app.getPeerParticipantList(data.ProjectUUID, data.SiteUUID) + if err != nil { + return errors.New("failed to get peer participant list") + } + return projectService.HandleDataDismissal(data.ProjectUUID, data.DataUUID, otherSiteList) +} + +func (app *ProjectApp) getPeerParticipantList(projectUUID, siteUUID string) ([]service.ProjectParticipantSiteInfo, error) { + participantListInstance, err := app.ParticipantRepo.GetByProjectUUID(projectUUID) + if err != nil { + return nil, err + } + participantList := participantListInstance.([]entity.ProjectParticipant) + var otherSiteList []service.ProjectParticipantSiteInfo + for _, participant := range participantList { + if participant.SiteUUID != siteUUID && + (participant.Status == entity.ProjectParticipantStatusJoined || + participant.Status == entity.ProjectParticipantStatusOwner) { + siteInstance, err := app.SiteRepo.GetByUUID(participant.SiteUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to find site") + } + site := siteInstance.(*entity.Site) + otherSiteList = append(otherSiteList, service.ProjectParticipantSiteInfo{ + Name: site.Name, + Description: site.Description, + UUID: site.UUID, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: site.ServerName, + }) + } + } + return otherSiteList, nil +} + +// ProcessProjectClosing handles project closing event +func (app *ProjectApp) ProcessProjectClosing(projectUUID string) error { + projectService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + InvitationRepo: app.InvitationRepo, + ParticipantRepo: app.ParticipantRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrapf(err, "failed to find project") + } + project := projectInstance.(*entity.Project) + + otherSiteList, err := app.getPeerParticipantList(projectUUID, project.ManagingSiteUUID) + if err != nil { + return errors.New("failed to get peer participant list") + } + return projectService.HandleProjectClosing(projectUUID, otherSiteList) +} + +// ListProjectByParticipant returns information of projects related to the specified site +func (app *ProjectApp) ListProjectByParticipant(participantUUID string) (map[string]ProjectInfoWithStatus, error) { + if participantUUID == "" { + return nil, errors.New("missing participant uuid") + } + participantListInstance, err := app.ParticipantRepo.GetBySiteUUID(participantUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to list participant %s", participantUUID) + } + participantList := participantListInstance.([]entity.ProjectParticipant) + + projectMap := map[string]ProjectInfoWithStatus{} + for _, participant := range participantList { + projectInstance, err := app.ProjectRepo.GetByUUID(participant.ProjectUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get project info for project %s", participant.ProjectUUID) + } + project := projectInstance.(*entity.Project) + + projectInfo := ProjectInfoWithStatus{ + ProjectUUID: project.UUID, + ProjectName: project.Name, + ProjectDescription: project.Description, + ProjectAutoApprovalEnabled: project.AutoApprovalEnabled, + ProjectManager: project.Manager, + ProjectManagingSiteName: project.ManagingSiteName, + ProjectManagingSitePartyID: project.ManagingSitePartyID, + ProjectManagingSiteUUID: project.ManagingSiteUUID, + ProjectCreationTime: project.CreatedAt, + ProjectStatus: 0, + } + + // ignore participant status for closed project + if project.Status == entity.ProjectStatusClosed { + projectInfo.ProjectStatus = entity.ProjectStatusClosed + } else { + switch participant.Status { + case entity.ProjectParticipantStatusPending: + projectInfo.ProjectStatus = entity.ProjectStatusPending + case entity.ProjectParticipantStatusJoined: + projectInfo.ProjectStatus = entity.ProjectStatusJoined + case entity.ProjectParticipantStatusDismissed: + projectInfo.ProjectStatus = entity.ProjectStatusDismissed + case entity.ProjectParticipantStatusLeft: + projectInfo.ProjectStatus = entity.ProjectStatusLeft + case entity.ProjectParticipantStatusRevoked: + projectInfo.ProjectStatus = entity.ProjectStatusDismissed + case entity.ProjectParticipantStatusRejected: + projectInfo.ProjectStatus = entity.ProjectStatusRejected + case entity.ProjectParticipantStatusOwner: + projectInfo.ProjectStatus = entity.ProjectStatusManaged + case entity.ProjectParticipantStatusUnknown: + projectInfo.ProjectStatus = entity.ProjectStatusClosed + } + } + projectMap[projectInfo.ProjectUUID] = projectInfo + } + return projectMap, err +} + +// ListDataAssociationByProject returns current associated data for certain project +func (app *ProjectApp) ListDataAssociationByProject(projectUUID string) (map[string]ProjectDataAssociation, error) { + if projectUUID == "" { + return nil, errors.New("missing project uuid") + } + dataListInstance, err := app.ProjectDataRepo.GetListByProjectUUID(projectUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to load data assoication") + } + dataList := dataListInstance.([]entity.ProjectData) + + projectDataMap := map[string]ProjectDataAssociation{} + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + projectDataMap[data.DataUUID] = ProjectDataAssociation{ + ProjectDataAssociationBase: ProjectDataAssociationBase{ + DataUUID: data.DataUUID, + }, + Name: data.Name, + Description: data.Description, + SiteName: data.SiteName, + SiteUUID: data.SiteUUID, + SitePartyID: data.SitePartyID, + TableName: data.TableName, + TableNamespace: data.TableNamespace, + CreationTime: data.CreationTime, + UpdateTime: data.UpdateTime, + } + } + } + return projectDataMap, err +} + +// ListParticipantByProject returns participant list in a project +func (app *ProjectApp) ListParticipantByProject(projectUUID string) (map[string]entity.ProjectParticipant, error) { + participantListInstance, err := app.ParticipantRepo.GetByProjectUUID(projectUUID) + if err != nil { + return nil, err + } + participantList := participantListInstance.([]entity.ProjectParticipant) + participantMap := map[string]entity.ProjectParticipant{} + for index, participant := range participantList { + participantMap[participant.SiteUUID] = participantList[index] + } + return participantMap, nil +} diff --git a/fml-manager/server/application/service/site_service.go b/fml-manager/server/application/service/site_service.go new file mode 100644 index 00000000..41f0d915 --- /dev/null +++ b/fml-manager/server/application/service/site_service.go @@ -0,0 +1,41 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/service" +) + +// SiteApp provide functions to manage the sites +type SiteApp struct { + // SiteRepo is the repository for persisting site info + SiteRepo repo.SiteRepository +} + +// RegisterSite creates or updates the site info +func (app *SiteApp) RegisterSite(site *entity.Site) error { + siteService := &service.SiteService{ + SiteRepo: app.SiteRepo, + } + return siteService.HandleSiteRegistration(site) +} + +// GetSiteList returns all saved sites info +func (app *SiteApp) GetSiteList() ([]entity.Site, error) { + list, err := app.SiteRepo.GetSiteList() + return list.([]entity.Site), err +} diff --git a/fml-manager/server/constants/common.go b/fml-manager/server/constants/common.go new file mode 100644 index 00000000..9ae1e389 --- /dev/null +++ b/fml-manager/server/constants/common.go @@ -0,0 +1,28 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +const APIVersion = "v1" + +var ( + // Branch is the source branch + Branch string + + // Commit is the commit number + Commit string + + // BuildTime is the compiling time + BuildTime string +) diff --git a/fml-manager/server/constants/response_code.go b/fml-manager/server/constants/response_code.go new file mode 100644 index 00000000..bad13506 --- /dev/null +++ b/fml-manager/server/constants/response_code.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +const ( + // RespNoErr means a success operation + RespNoErr = 0 + // RespInternalErr means some error occurred + RespInternalErr = 10001 +) diff --git a/fml-manager/server/docs/docs.go b/fml-manager/server/docs/docs.go new file mode 100644 index 00000000..f39e7117 --- /dev/null +++ b/fml-manager/server/docs/docs.go @@ -0,0 +1,1409 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/job/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job creation", + "parameters": [ + { + "description": "job creation request", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobRemoteJobCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/response": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job response", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "job approval response", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobApprovalContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/status": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job status update", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "job status", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobStatusUpdateContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all project", + "parameters": [ + { + "type": "string", + "description": "participant uuid, if set, only returns the projects containing the participant", + "name": "participant", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectInfoWithStatus" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/event/participant/update": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info update event, called by this FML manager's site context only", + "parameters": [ + { + "description": "Updated participant info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/event.ProjectParticipantUpdateEvent" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project invitation", + "parameters": [ + { + "description": "invitation request", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectInvitationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/accept": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation acceptance response", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation rejection response", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/revoke": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation revocation request", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project closing", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all data association in a project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/associate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process new data association from site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data association info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process data dismissal from site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data association info containing the data UUID", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociationBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all participants in a project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{siteUUID}/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant dismissal, called by the managing site only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{siteUUID}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant leaving", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Return sites list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Site" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Create or update site info", + "parameters": [ + { + "description": "The site information", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.Site" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.Site": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "description": "Description contains more text about this site", + "type": "string" + }, + "external_host": { + "description": "ExternalHost is the IP or hostname this site portal service is exposed", + "type": "string" + }, + "external_port": { + "description": "ExternalPort the port number this site portal service is exposed", + "type": "integer" + }, + "https": { + "description": "HTTPS indicate whether the endpoint is over https", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "last_connected_at": { + "description": "LastRegisteredAt is the last time this site has tried to register to the manager", + "type": "string" + }, + "name": { + "description": "Name is the site's name", + "type": "string" + }, + "party_id": { + "description": "PartyID is the id of this party", + "type": "integer" + }, + "server_name": { + "description": "ServerName is used by fml manager to verify endpoint's certificate when HTTPS is enabled", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "event.ProjectParticipantUpdateEvent": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "service.JobApprovalContext": { + "type": "object", + "properties": { + "approved": { + "type": "boolean" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobDataBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, + "service.JobRemoteJobCreationRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobStatusUpdateContext": { + "type": "object", + "properties": { + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_id": { + "type": "string" + }, + "fate_model_version": { + "type": "string" + }, + "participant_status_map": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + } + } + }, + "service.ProjectDataAssociation": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_uuid": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "table_name": { + "type": "string" + }, + "table_namespace": { + "type": "string" + }, + "update_time": { + "type": "string" + } + } + }, + "service.ProjectDataAssociationBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + } + } + }, + "service.ProjectInfoWithStatus": { + "type": "object", + "properties": { + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_status": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + } + } + }, + "service.ProjectInvitationRequest": { + "type": "object", + "properties": { + "associated_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + }, + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "fml manager API service", + Description: "backend APIs of fml manager service", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/fml-manager/server/docs/swagger.json b/fml-manager/server/docs/swagger.json new file mode 100644 index 00000000..e2f18a13 --- /dev/null +++ b/fml-manager/server/docs/swagger.json @@ -0,0 +1,1385 @@ +{ + "swagger": "2.0", + "info": { + "description": "backend APIs of fml manager service", + "title": "fml manager API service", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "v1" + }, + "basePath": "/api/v1", + "paths": { + "/job/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job creation", + "parameters": [ + { + "description": "job creation request", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobRemoteJobCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/response": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job response", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "job approval response", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobApprovalContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/status": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Process job status update", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "job status", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobStatusUpdateContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all project", + "parameters": [ + { + "type": "string", + "description": "participant uuid, if set, only returns the projects containing the participant", + "name": "participant", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectInfoWithStatus" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/event/participant/update": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info update event, called by this FML manager's site context only", + "parameters": [ + { + "description": "Updated participant info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/event.ProjectParticipantUpdateEvent" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project invitation", + "parameters": [ + { + "description": "invitation request", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectInvitationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/accept": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation acceptance response", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation rejection response", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/invitation/{uuid}/revoke": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation revocation request", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project closing", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all data association in a project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/associate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process new data association from site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data association info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process data dismissal from site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data association info containing the data UUID", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociationBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all participants in a project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{siteUUID}/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant dismissal, called by the managing site only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{siteUUID}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant leaving", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Return sites list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Site" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Create or update site info", + "parameters": [ + { + "description": "The site information", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.Site" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.Site": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "description": "Description contains more text about this site", + "type": "string" + }, + "external_host": { + "description": "ExternalHost is the IP or hostname this site portal service is exposed", + "type": "string" + }, + "external_port": { + "description": "ExternalPort the port number this site portal service is exposed", + "type": "integer" + }, + "https": { + "description": "HTTPS indicate whether the endpoint is over https", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "last_connected_at": { + "description": "LastRegisteredAt is the last time this site has tried to register to the manager", + "type": "string" + }, + "name": { + "description": "Name is the site's name", + "type": "string" + }, + "party_id": { + "description": "PartyID is the id of this party", + "type": "integer" + }, + "server_name": { + "description": "ServerName is used by fml manager to verify endpoint's certificate when HTTPS is enabled", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "event.ProjectParticipantUpdateEvent": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "service.JobApprovalContext": { + "type": "object", + "properties": { + "approved": { + "type": "boolean" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobDataBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, + "service.JobRemoteJobCreationRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobStatusUpdateContext": { + "type": "object", + "properties": { + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_id": { + "type": "string" + }, + "fate_model_version": { + "type": "string" + }, + "participant_status_map": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + } + } + }, + "service.ProjectDataAssociation": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_uuid": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "table_name": { + "type": "string" + }, + "table_namespace": { + "type": "string" + }, + "update_time": { + "type": "string" + } + } + }, + "service.ProjectDataAssociationBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + } + } + }, + "service.ProjectInfoWithStatus": { + "type": "object", + "properties": { + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_status": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + } + } + }, + "service.ProjectInvitationRequest": { + "type": "object", + "properties": { + "associated_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectDataAssociation" + } + }, + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/fml-manager/server/docs/swagger.yaml b/fml-manager/server/docs/swagger.yaml new file mode 100644 index 00000000..cdaad3af --- /dev/null +++ b/fml-manager/server/docs/swagger.yaml @@ -0,0 +1,849 @@ +basePath: /api/v1 +definitions: + api.GeneralResponse: + properties: + code: + example: 0 + type: integer + data: + type: object + message: + example: success + type: string + type: object + entity.Site: + properties: + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + description: + description: Description contains more text about this site + type: string + external_host: + description: ExternalHost is the IP or hostname this site portal service is + exposed + type: string + external_port: + description: ExternalPort the port number this site portal service is exposed + type: integer + https: + description: HTTPS indicate whether the endpoint is over https + type: boolean + id: + type: integer + last_connected_at: + description: LastRegisteredAt is the last time this site has tried to register + to the manager + type: string + name: + description: Name is the site's name + type: string + party_id: + description: PartyID is the id of this party + type: integer + server_name: + description: ServerName is used by fml manager to verify endpoint's certificate + when HTTPS is enabled + type: string + updatedAt: + type: string + uuid: + type: string + type: object + event.ProjectParticipantUpdateEvent: + properties: + description: + type: string + name: + type: string + party_id: + type: integer + uuid: + type: string + type: object + gorm.DeletedAt: + properties: + time: + type: string + valid: + description: Valid is true if Time is not NULL + type: boolean + type: object + service.JobApprovalContext: + properties: + approved: + type: boolean + site_uuid: + type: string + type: object + service.JobDataBase: + properties: + data_uuid: + type: string + label_name: + type: string + type: object + service.JobRemoteJobCreationRequest: + properties: + algorithm_component_name: + type: string + conf_json: + type: string + description: + type: string + dsl_json: + type: string + evaluate_component_name: + type: string + initiator_data: + $ref: '#/definitions/service.JobDataBase' + name: + type: string + other_site_data: + items: + $ref: '#/definitions/service.JobDataBase' + type: array + predicting_model_uuid: + type: string + project_uuid: + type: string + training_algorithm_type: + type: integer + training_component_list_to_deploy: + items: + type: string + type: array + training_model_name: + type: string + training_validation_enabled: + type: boolean + training_validation_percent: + type: integer + type: + type: integer + username: + type: string + uuid: + type: string + type: object + service.JobStatusUpdateContext: + properties: + fate_job_id: + type: string + fate_job_status: + type: string + fate_model_id: + type: string + fate_model_version: + type: string + participant_status_map: + additionalProperties: + type: integer + type: object + status: + type: integer + status_message: + type: string + type: object + service.ProjectDataAssociation: + properties: + creation_time: + type: string + data_uuid: + type: string + description: + type: string + name: + type: string + site_name: + type: string + site_party_id: + type: integer + site_uuid: + type: string + table_name: + type: string + table_namespace: + type: string + update_time: + type: string + type: object + service.ProjectDataAssociationBase: + properties: + data_uuid: + type: string + type: object + service.ProjectInfoWithStatus: + properties: + project_auto_approval_enabled: + type: boolean + project_creation_time: + type: string + project_description: + type: string + project_manager: + type: string + project_managing_site_name: + type: string + project_managing_site_party_id: + type: integer + project_managing_site_uuid: + type: string + project_name: + type: string + project_status: + type: integer + project_uuid: + type: string + type: object + service.ProjectInvitationRequest: + properties: + associated_data: + items: + $ref: '#/definitions/service.ProjectDataAssociation' + type: array + project_auto_approval_enabled: + type: boolean + project_creation_time: + type: string + project_description: + type: string + project_manager: + type: string + project_managing_site_name: + type: string + project_managing_site_party_id: + type: integer + project_managing_site_uuid: + type: string + project_name: + type: string + project_uuid: + type: string + site_party_id: + type: integer + site_uuid: + type: string + uuid: + type: string + type: object +info: + contact: + name: FedLCM team + description: backend APIs of fml manager service + termsOfService: http://swagger.io/terms/ + title: fml manager API service + version: v1 +paths: + /job/{uuid}/response: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + - description: job approval response + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.JobApprovalContext' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process job response + tags: + - Job + /job/{uuid}/status: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + - description: job status + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.JobStatusUpdateContext' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process job status update + tags: + - Job + /job/create: + post: + parameters: + - description: job creation request + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.JobRemoteJobCreationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process job creation + tags: + - Job + /project: + get: + parameters: + - description: participant uuid, if set, only returns the projects containing + the participant + in: query + name: participant + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + additionalProperties: + $ref: '#/definitions/service.ProjectInfoWithStatus' + type: object + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all project + tags: + - Project + /project/{uuid}/close: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process project closing + tags: + - Project + /project/{uuid}/data: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + additionalProperties: + $ref: '#/definitions/service.ProjectDataAssociation' + type: object + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all data association in a project + tags: + - Project + /project/{uuid}/data/associate: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Data association info + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.ProjectDataAssociation' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process new data association from site + tags: + - Project + /project/{uuid}/data/dismiss: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Data association info containing the data UUID + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.ProjectDataAssociationBase' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process data dismissal from site + tags: + - Project + /project/{uuid}/participant: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + additionalProperties: + $ref: '#/definitions/service.ProjectDataAssociation' + type: object + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all participants in a project + tags: + - Project + /project/{uuid}/participant/{siteUUID}/dismiss: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Site UUID + in: path + name: siteUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant dismissal, called by the managing site only + tags: + - Project + /project/{uuid}/participant/{siteUUID}/leave: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Site UUID + in: path + name: siteUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant leaving + tags: + - Project + /project/event/participant/update: + post: + parameters: + - description: Updated participant info + in: body + name: project + required: true + schema: + $ref: '#/definitions/event.ProjectParticipantUpdateEvent' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant info update event, called by this FML manager's + site context only + tags: + - Project + /project/invitation: + post: + parameters: + - description: invitation request + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.ProjectInvitationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process project invitation + tags: + - Project + /project/invitation/{uuid}/accept: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation acceptance response + tags: + - Project + /project/invitation/{uuid}/reject: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation rejection response + tags: + - Project + /project/invitation/{uuid}/revoke: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation revocation request + tags: + - Project + /site: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/entity.Site' + type: array + type: object + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return sites list + tags: + - Site + post: + parameters: + - description: The site information + in: body + name: site + required: true + schema: + $ref: '#/definitions/entity.Site' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create or update site info + tags: + - Site +swagger: "2.0" diff --git a/fml-manager/server/domain/entity/job.go b/fml-manager/server/domain/entity/job.go new file mode 100644 index 00000000..6bbde7a0 --- /dev/null +++ b/fml-manager/server/domain/entity/job.go @@ -0,0 +1,181 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// Job represents a FATE job +type Job struct { + gorm.Model + Name string `json:"name" gorm:"type:varchar(255)"` + Description string `json:"description" gorm:"type:text"` + UUID string `json:"uuid" gorm:"type:varchar(36)"` + ProjectUUID string `json:"project_uuid" gorm:"type:varchar(36)"` + Type JobType `json:"type"` + Status JobStatus `json:"status"` + StatusMessage string `gorm:"type:text"` + AlgorithmType JobAlgorithmType + AlgorithmConfig AlgorithmConfig `gorm:"type:text"` + ModelName string `json:"model_name" gorm:"type:varchar(255)"` + PredictingModelUUID string `gorm:"type:varchar(36)"` + InitiatingSiteUUID string `gorm:"type:varchar(36)"` + InitiatingSiteName string `gorm:"type:varchar(255)"` + InitiatingSitePartyID uint + InitiatingUser string `gorm:"type:varchar(255)"` + FATEJobID string `gorm:"type:varchar(255);column:fate_job_id"` + FATEJobStatus string `gorm:"type:varchar(36);column:fate_job_status"` + FATEModelID string `gorm:"type:varchar(255);column:fate_model_id"` + FATEModelVersion string `gorm:"type:varchar(255);column:fate_model_version"` + Conf string `gorm:"type:text"` + DSL string `gorm:"type:text"` + RequestJson string `gorm:"type:text"` + FinishedAt time.Time + Repo repo.JobRepository `gorm:"-"` +} + +// JobStatus is the enum of job status +type JobStatus uint8 + +const ( + JobStatusUnknown JobStatus = iota + JobStatusPending + JobStatusRejected + JobStatusRunning + JobStatusFailed + JobStatusSucceeded +) + +func (s JobStatus) String() string { + names := map[JobStatus]string{ + JobStatusUnknown: "Unknown", + JobStatusPending: "Pending", + JobStatusRejected: "Rejected", + JobStatusRunning: "Running", + JobStatusFailed: "Failed", + JobStatusSucceeded: "Succeeded", + } + return names[s] +} + +// JobType is the enum of job type +type JobType uint8 + +const ( + JobTypeUnknown JobType = iota + JobTypeTraining + JobTypePredict + JobTypePSI +) + +func (t JobType) String() string { + names := map[JobType]string{ + JobTypeUnknown: "Unknown", + JobTypeTraining: "Modeling", + JobTypePredict: "Predict", + JobTypePSI: "PSI", + } + return names[t] +} + +// JobAlgorithmType is the enum of the job algorithm +type JobAlgorithmType uint8 + +const ( + JobAlgorithmTypeUnknown JobAlgorithmType = iota + JobAlgorithmTypeHomoLR + JobAlgorithmTypeHomoSBT +) + +// AlgorithmConfig contains algorithm configuration settings for the job +type AlgorithmConfig struct { + TrainingValidationEnabled bool `json:"training_validation_enabled"` + TrainingValidationSizePercent uint `json:"training_validation_percent"` + TrainingComponentsToDeploy []string `json:"training_component_list_to_deploy"` +} + +func (c AlgorithmConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *AlgorithmConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// Create initializes the job and save into the repo. +func (job *Job) Create() error { + if job.UUID == "" { + return errors.New("job must contains a valid uuid") + } + job.Status = JobStatusPending + job.Model = gorm.Model{} + job.FATEModelID = "" + job.FATEJobID = "" + job.FATEJobStatus = "" + if err := job.Repo.Create(job); err != nil { + return err + } + return nil +} + +// Update updates the job info, including the fate job status. +func (job *Job) Update(newStatus *Job) error { + if job.FATEJobID == "" || job.FATEJobStatus != newStatus.FATEJobStatus { + job.FATEJobID = newStatus.FATEJobID + job.FATEJobStatus = newStatus.FATEJobStatus + job.FATEModelID = newStatus.FATEModelID + job.FATEModelVersion = newStatus.FATEModelVersion + if err := job.Repo.UpdateFATEJobInfoByUUID(job); err != nil { + return errors.Wrap(err, "failed to update FATE job info") + } + } + if job.Status != newStatus.Status { + if err := job.UpdateStatus(newStatus.Status); err != nil { + return err + } + } + if job.StatusMessage != newStatus.StatusMessage { + if err := job.UpdateStatusMessage(newStatus.StatusMessage); err != nil { + return err + } + } + return nil +} + +// UpdateStatus updates the job's status +func (job *Job) UpdateStatus(status JobStatus) error { + job.Status = status + if err := job.Repo.UpdateStatusByUUID(job); err != nil { + return errors.Wrap(err, "failed to update job status") + } + return nil +} + +// UpdateStatusMessage updates the job's status message +func (job *Job) UpdateStatusMessage(message string) error { + job.StatusMessage = message + if err := job.Repo.UpdateStatusMessageByUUID(job); err != nil { + return errors.Wrap(err, "failed to update job status message") + } + return nil +} diff --git a/fml-manager/server/domain/entity/job_participant.go b/fml-manager/server/domain/entity/job_participant.go new file mode 100644 index 00000000..daac514e --- /dev/null +++ b/fml-manager/server/domain/entity/job_participant.go @@ -0,0 +1,83 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// JobParticipant represents a site and its data for a job +type JobParticipant struct { + gorm.Model + UUID string `gorm:"type:varchar(36)"` + JobUUID string `gorm:"type:varchar(36)"` + SiteUUID string `gorm:"type:varchar(36)"` + SiteName string `gorm:"type:varchar(255)"` + SitePartyID uint + DataUUID string `gorm:"type:varchar(36)"` + DataName string `gorm:"type:varchar(255)"` + DataDescription string `gorm:"type:text"` + DataTableName string `gorm:"type:varchar(255)"` + DataTableNamespace string `gorm:"type:varchar(255)"` + DataLabelName string `gorm:"type:varchar(255)"` + Status JobParticipantStatus + Repo repo.JobParticipantRepository `gorm:"-"` +} + +// JobParticipantStatus is the status of this participant in the job +type JobParticipantStatus uint8 + +const ( + JobParticipantStatusUnknown = iota + JobParticipantStatusInitiator + JobParticipantStatusPending + JobParticipantStatusApproved + JobParticipantStatusRejected +) + +func (s JobParticipantStatus) String() string { + names := map[JobParticipantStatus]string{ + JobParticipantStatusUnknown: "Unknown", + JobParticipantStatusPending: "Pending", + JobParticipantStatusRejected: "Rejected", + JobParticipantStatusApproved: "Approved", + JobParticipantStatusInitiator: "Auto-approved as Initiator", + } + return names[s] +} + +// Create initialize the participant info and create it in the repo +func (p *JobParticipant) Create() error { + p.UUID = uuid.NewV4().String() + if err := p.Repo.Create(p); err != nil { + return err + } + return nil +} + +// UpdateStatus changes the participant's status +func (p *JobParticipant) UpdateStatus(status JobParticipantStatus) error { + if p.Status != status { + p.Status = status + return p.Repo.UpdateStatusByUUID(p) + } + return nil +} + +func (p *JobParticipant) GetStatus() int { + return p.Repo.GetStatusByUUID(p).(int) +} diff --git a/fml-manager/server/domain/entity/project.go b/fml-manager/server/domain/entity/project.go new file mode 100644 index 00000000..f60ee5b3 --- /dev/null +++ b/fml-manager/server/domain/entity/project.go @@ -0,0 +1,47 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/valueobject" + "gorm.io/gorm" +) + +// Project contains jobs, sites and their data for collaborating via FATE +type Project struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `json:"description" gorm:"type:text"` + AutoApprovalEnabled bool `json:"auto_approval_enabled"` + Status ProjectStatus + *valueobject.ProjectCreatorInfo + Repo repo.ProjectRepository `json:"-" gorm:"-"` +} + +// ProjectStatus is the status of a project +type ProjectStatus uint8 + +const ( + ProjectStatusUnknown ProjectStatus = iota + ProjectStatusManaged + ProjectStatusPending + ProjectStatusJoined + ProjectStatusRejected + ProjectStatusLeft + ProjectStatusClosed + ProjectStatusDismissed +) diff --git a/fml-manager/server/domain/entity/project_data.go b/fml-manager/server/domain/entity/project_data.go new file mode 100644 index 00000000..82d09814 --- /dev/null +++ b/fml-manager/server/domain/entity/project_data.go @@ -0,0 +1,49 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "gorm.io/gorm" + "time" +) + +// ProjectData is a data association from a site to a project +type ProjectData struct { + gorm.Model + Name string `gorm:"type:varchar(255)"` + Description string `gorm:"type:text"` + UUID string `gorm:"type:varchar(36)"` + ProjectUUID string `gorm:"type:varchar(36)"` + DataUUID string `gorm:"type:varchar(36)"` + SiteUUID string `gorm:"type:varchar(36)"` + SiteName string `gorm:"type:varchar(255)"` + SitePartyID uint + Status ProjectDataStatus + TableName string `gorm:"type:varchar(255)"` + TableNamespace string `gorm:"type:varchar(255)"` + CreationTime time.Time + UpdateTime time.Time + Repo repo.ProjectDataRepository `gorm:"-"` +} + +// ProjectDataStatus is the status of the association +type ProjectDataStatus uint8 + +const ( + ProjectDataStatusUnknown ProjectDataStatus = iota + ProjectDataStatusDismissed + ProjectDataStatusAssociated +) diff --git a/fml-manager/server/domain/entity/project_invitation.go b/fml-manager/server/domain/entity/project_invitation.go new file mode 100644 index 00000000..036d35e5 --- /dev/null +++ b/fml-manager/server/domain/entity/project_invitation.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// ProjectInvitation is an invitation from a project owner to a site +type ProjectInvitation struct { + gorm.Model + UUID string `gorm:"type:varchar(36)"` + ProjectUUID string `gorm:"type:varchar(36)"` + SiteUUID string `gorm:"type:varchar(36)"` + Status ProjectInvitationStatus +} + +// ProjectInvitationStatus is the status of the invitation +type ProjectInvitationStatus uint8 + +const ( + ProjectInvitationStatusCreated ProjectInvitationStatus = iota + ProjectInvitationStatusSent + ProjectInvitationStatusRevoked + ProjectInvitationStatusAccepted + ProjectInvitationStatusRejected +) diff --git a/fml-manager/server/domain/entity/project_participant.go b/fml-manager/server/domain/entity/project_participant.go new file mode 100644 index 00000000..739137dd --- /dev/null +++ b/fml-manager/server/domain/entity/project_participant.go @@ -0,0 +1,43 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// ProjectParticipant is a site joining a project +type ProjectParticipant struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36)"` + ProjectUUID string `json:"project_uuid" gorm:"type:varchar(36)"` + SiteUUID string `json:"site_uuid" gorm:"type:varchar(36)"` + SiteName string `json:"site_name" gorm:"type:varchar(255)"` + SitePartyID uint `json:"site_party_id"` + SiteDescription string `json:"site_description"` + Status ProjectParticipantStatus `json:"status"` +} + +// ProjectParticipantStatus is the status of the current participant +type ProjectParticipantStatus uint8 + +const ( + ProjectParticipantStatusUnknown ProjectParticipantStatus = iota + ProjectParticipantStatusOwner + ProjectParticipantStatusPending + ProjectParticipantStatusJoined + ProjectParticipantStatusRejected + ProjectParticipantStatusLeft + ProjectParticipantStatusDismissed + ProjectParticipantStatusRevoked +) diff --git a/fml-manager/server/domain/entity/site.go b/fml-manager/server/domain/entity/site.go new file mode 100644 index 00000000..20999cfc --- /dev/null +++ b/fml-manager/server/domain/entity/site.go @@ -0,0 +1,46 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "time" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "gorm.io/gorm" +) + +// Site contains all the info for the current site +type Site struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36);index;unique"` + // Name is the site's name + Name string `json:"name" gorm:"type:varchar(255);unique;not null"` + // Description contains more text about this site + Description string `json:"description" gorm:"type:text"` + // PartyID is the id of this party + PartyID uint `json:"party_id" gorm:"column:party_id"` + // ExternalHost is the IP or hostname this site portal service is exposed + ExternalHost string `json:"external_host" gorm:"type:varchar(255);column:external_ip"` + // ExternalPort the port number this site portal service is exposed + ExternalPort uint `json:"external_port" gorm:"column:external_port"` + // HTTPS indicate whether the endpoint is over https + HTTPS bool `json:"https"` + // ServerName is used by fml manager to verify endpoint's certificate when HTTPS is enabled + ServerName string `json:"server_name"` + // LastRegisteredAt is the last time this site has tried to register to the manager + LastRegisteredAt time.Time `json:"last_connected_at"` + // Repo is the repository interface + Repo repo.SiteRepository `json:"-" gorm:"-"` +} diff --git a/fml-manager/server/domain/repo/job_participant_repo.go b/fml-manager/server/domain/repo/job_participant_repo.go new file mode 100644 index 00000000..5e540e7b --- /dev/null +++ b/fml-manager/server/domain/repo/job_participant_repo.go @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "github.com/pkg/errors" +) + +// ErrJobParticipantNotFound is an error returned when no participant record is found +var ErrJobParticipantNotFound = errors.New("this site is not in the current job") + +// JobParticipantRepository is the interface for managing job participant in the repo +type JobParticipantRepository interface { + // Create takes an *entity.JobParticipant and save it in the repo + Create(interface{}) error + // UpdateStatusByUUID takes an *entity.JobParticipant and updates the status in the repo + UpdateStatusByUUID(interface{}) error + // GetStatusByUUID takes an *entity.JobParticipant and returns the status of the participant + GetStatusByUUID(instance interface{}) interface{} + // GetByJobAndSiteUUID returns an *entity.JobParticipant indexed by the job and site uuid + GetByJobAndSiteUUID(string, string) (interface{}, error) + // GetListByJobUUID returns an []entity.JobParticipant list in the specified job + GetListByJobUUID(string) (interface{}, error) +} diff --git a/fml-manager/server/domain/repo/job_repo.go b/fml-manager/server/domain/repo/job_repo.go new file mode 100644 index 00000000..4874f458 --- /dev/null +++ b/fml-manager/server/domain/repo/job_repo.go @@ -0,0 +1,46 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrJobNotFound is the error returned when no project is found +var ErrJobNotFound = errors.New("job not found") + +// JobRepository is the interface to manage job info in the repo +type JobRepository interface { + // Create takes an *entity.Job and creates it in the repo + Create(interface{}) error + // UpdateFATEJobInfoByUUID takes an *entity.Job and updates the FATE job related info + UpdateFATEJobInfoByUUID(interface{}) error + // UpdateFATEJobStatusByUUID takes an *entity.Job and updates the FATE job status field + UpdateFATEJobStatusByUUID(interface{}) error + // UpdateStatusByUUID takes an *entity.Job and updates the job status field + UpdateStatusByUUID(interface{}) error + // CheckNameConflict returns error if the same name job exists + CheckNameConflict(string) error + // GetAll returns []entity.Job of all not-deleted jobs + GetAll() (interface{}, error) + // DeleteByProjectUUID delete the job of the specified project + DeleteByProjectUUID(string) error + // GetListByProjectUUID returns a list of []entity.Job in the specified project + GetListByProjectUUID(string) (interface{}, error) + // GetByUUID returns an *entity.Job of the specified uuid + GetByUUID(string) (interface{}, error) + // UpdateStatusMessageByUUID takes an *entity.Job and updates the job status message field + UpdateStatusMessageByUUID(interface{}) error + // UpdateFinishTimeByUUID takes an *entity.Job and updates the finish time + UpdateFinishTimeByUUID(interface{}) error +} diff --git a/fml-manager/server/domain/repo/project_data_repo.go b/fml-manager/server/domain/repo/project_data_repo.go new file mode 100644 index 00000000..beb85800 --- /dev/null +++ b/fml-manager/server/domain/repo/project_data_repo.go @@ -0,0 +1,42 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "github.com/pkg/errors" +) + +// ErrProjectDataNotFound is an error when no data association record found +var ErrProjectDataNotFound = errors.New("this data is not in the specified project") + +// ProjectDataRepository is the repo interface for persisting the project data info +type ProjectDataRepository interface { + // Create takes an *entity.ProjectData and create the records + Create(interface{}) error + // GetByProjectAndDataUUID returns an *entity.ProjectData indexed by the specified project and data uuid + GetByProjectAndDataUUID(string, string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.ProjectData and update its status + UpdateStatusByUUID(interface{}) error + // GetListByProjectUUID returns []entity.ProjectData that associated in the specified project + GetListByProjectUUID(string) (interface{}, error) + // GetListByProjectAndSiteUUID returns []entity.ProjectData that associated in the specified project by the specified site + GetListByProjectAndSiteUUID(string, string) (interface{}, error) + // DeleteByUUID delete the project data records by the specified uuid + DeleteByUUID(string) error + // DeleteByProjectUUID delete the project data records permanently by the specified project uuid + DeleteByProjectUUID(string) error + // UpdateSiteInfoBySiteUUID takes an *entity.ProjectData as template to update site info of the specified site uuid + UpdateSiteInfoBySiteUUID(interface{}) error +} diff --git a/fml-manager/server/domain/repo/project_invitation_repo.go b/fml-manager/server/domain/repo/project_invitation_repo.go new file mode 100644 index 00000000..f41728b4 --- /dev/null +++ b/fml-manager/server/domain/repo/project_invitation_repo.go @@ -0,0 +1,27 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ProjectInvitationRepository is an interface for working with the invitation repo +type ProjectInvitationRepository interface { + // Create takes an *entity.ProjectInvitation to create the record + Create(interface{}) error + // UpdateStatusByUUID takes an *entity.ProjectInvitation and updates the status + UpdateStatusByUUID(interface{}) error + // GetByProjectUUID returns an *entity.ProjectInvitation for the specified project. it is the latest one for the project + GetByProjectUUID(string) (interface{}, error) + // GetByUUID returns an *entity.ProjectInvitation indexed by the specified uuid + GetByUUID(string) (interface{}, error) +} diff --git a/fml-manager/server/domain/repo/project_participant_repo.go b/fml-manager/server/domain/repo/project_participant_repo.go new file mode 100644 index 00000000..28ecf686 --- /dev/null +++ b/fml-manager/server/domain/repo/project_participant_repo.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrProjectParticipantNotFound is an error returned when no participant record is found +var ErrProjectParticipantNotFound = errors.New("this site is not in the current project") + +// ProjectParticipantRepository is the interface to work with participant related repo +type ProjectParticipantRepository interface { + // GetByProjectUUID returns []entity.ProjectParticipant of the specified project + GetByProjectUUID(string) (interface{}, error) + // Create takes an *entity.ProjectParticipant cam create the records + Create(interface{}) error + // GetByProjectAndSiteUUID returns an *entity.ProjectParticipant from the specified project and site uuid + GetByProjectAndSiteUUID(string, string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.ProjectParticipant and update its status + UpdateStatusByUUID(interface{}) error + // UpdateParticipantInfoBySiteUUID takes an *entity.ProjectParticipant as template and + // updates sites info of the records containing the specified site uuid + UpdateParticipantInfoBySiteUUID(interface{}) error + // GetBySiteUUID returns a []entity.ProjectParticipant of the specified site + GetBySiteUUID(string) (interface{}, error) +} diff --git a/fml-manager/server/domain/repo/project_repo.go b/fml-manager/server/domain/repo/project_repo.go new file mode 100644 index 00000000..3f57a443 --- /dev/null +++ b/fml-manager/server/domain/repo/project_repo.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "github.com/pkg/errors" +) + +// ErrProjectNotFound is the error returned when no project is found +var ErrProjectNotFound = errors.New("project not found") + +// ProjectRepository is the repo interface for project +type ProjectRepository interface { + // Create takes an *entity.Project and create the record + Create(interface{}) error + // GetAll returns an []entity.Project + GetAll() (interface{}, error) + // GetByUUID returns an *entity.Project with the specified uuid + GetByUUID(string) (interface{}, error) + // UpdateManagingSiteInfoBySiteUUID takes an *entity.Project as template and + // updates site related info of all records containing the site uuid + UpdateManagingSiteInfoBySiteUUID(interface{}) error + // UpdateStatusByUUID takes an *entity.Project and update its status + UpdateStatusByUUID(interface{}) error +} diff --git a/fml-manager/server/domain/repo/site_repo.go b/fml-manager/server/domain/repo/site_repo.go new file mode 100644 index 00000000..67e0b41a --- /dev/null +++ b/fml-manager/server/domain/repo/site_repo.go @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrSiteNameConflict means new site cannot be created due to the existence of the same-name site +var ErrSiteNameConflict = errors.New("a site with the same name but different UUID is already registered") + +// SiteRepository is the interface to handle site related information in the repo +type SiteRepository interface { + // GetSiteList returns all sites in []entity.Site + GetSiteList() (interface{}, error) + // Save creates a site info record in the repository + Save(instance interface{}) (interface{}, error) + // ExistByUUID returns whether the site with the uuid exists + ExistByUUID(uuid string) (bool, error) + // UpdateByUUID updates sites info by uuid + UpdateByUUID(instance interface{}) error + // DeleteByUUID delete sites info with the specified uuid + DeleteByUUID(uuid string) error + // GetByUUID returns an *entity.Site of the specified site + GetByUUID(string) (interface{}, error) +} diff --git a/fml-manager/server/domain/service/job_service.go b/fml-manager/server/domain/service/job_service.go new file mode 100644 index 00000000..67214f5c --- /dev/null +++ b/fml-manager/server/domain/service/job_service.go @@ -0,0 +1,189 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "fmt" + "sync" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/siteportal" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// JobService processes job related requests +type JobService struct { + JobRepo repo.JobRepository + ParticipantRepo repo.JobParticipantRepository +} + +// JobCreationRequest holds the job info and all the joined participants +type JobCreationRequest struct { + Job *entity.Job + Initiator JobParticipantSiteInfo + Participants map[string]JobParticipantSiteInfo +} + +// JobApprovalResponse contains the info needed to process the job approval response +type JobApprovalResponse struct { + Initiator JobParticipantConnectionInfo + Participants map[string]JobParticipantConnectionInfo + ApprovingSite *entity.JobParticipant + Approved bool + JobUUID string +} + +// JobParticipantSiteInfo contains more detailed info of a participating site +type JobParticipantSiteInfo struct { + *entity.JobParticipant + JobParticipantConnectionInfo +} + +// JobParticipantConnectionInfo contains connection info to the participating site +type JobParticipantConnectionInfo struct { + ExternalHost string + ExternalPort uint + HTTPS bool + ServerName string +} + +// JobParticipantStatusInfo contains job participating status and the site connection info +type JobParticipantStatusInfo struct { + entity.JobParticipantStatus + JobParticipantConnectionInfo +} + +// JobStatusUpdateContext contains info needed to update a job status +type JobStatusUpdateContext struct { + JobUUID string + NewJobStatus *entity.Job + ParticipantStatusMap map[string]JobParticipantStatusInfo + RequestJson string +} + +// HandleNewJobCreation process job creation request +func (s *JobService) HandleNewJobCreation(request *JobCreationRequest) error { + if err := request.Job.Create(); err != nil { + return err + } + if err := request.Initiator.Create(); err != nil { + return err + } + for _, participant := range request.Participants { + participant.JobUUID = request.Job.UUID + if err := participant.Create(); err != nil { + return errors.Wrapf(err, "failed to create participant: %s", participant.SiteUUID) + } + } + + wg := &sync.WaitGroup{} + var failedSite []string + for uuid := range request.Participants { + wg.Add(1) + go func(siteUUID string) { + defer wg.Done() + participant := request.Participants[siteUUID] + sitePortalClient := siteportal.NewSitePortalClient(participant.ExternalHost, participant.ExternalPort, participant.HTTPS, participant.ServerName) + if err := sitePortalClient.SendJobCreationRequest(request.Job.RequestJson); err != nil { + log.Err(err).Str("job uuid", request.Job.UUID).Str("site uuid", siteUUID).Msg("fail to send job creation to site") + failedSite = append(failedSite, fmt.Sprintf("%s(%s)", participant.SiteName, participant.SiteUUID)) + } + }(uuid) + } + wg.Wait() + if len(failedSite) != 0 { + return errors.Errorf("failed to send job creation to some site(s): %v", failedSite) + } + return nil +} + +// HandleJobApprovalResponse process job approval response +func (s *JobService) HandleJobApprovalResponse(response *JobApprovalResponse) error { + var newStatus entity.JobParticipantStatus + if response.Approved { + newStatus = entity.JobParticipantStatusApproved + } else { + newStatus = entity.JobParticipantStatusRejected + } + statusInDB := response.ApprovingSite.GetStatus() + log.Debug().Msgf("The status in DB is %d", statusInDB) + if statusInDB == entity.JobParticipantStatusApproved { + // This happens when the auto-approve thread comes, but there is a manual approve came earlier. + // In such case, just ignore this approval response from the participant's site portal. + return nil + } + if err := response.ApprovingSite.UpdateStatus(newStatus); err != nil { + return err + } + sitePortalClient := siteportal.NewSitePortalClient(response.Initiator.ExternalHost, response.Initiator.ExternalPort, response.Initiator.HTTPS, response.Initiator.ServerName) + if err := sitePortalClient.SendJobApprovalResponse(response.JobUUID, siteportal.JobApprovalContext{ + SiteUUID: response.ApprovingSite.SiteUUID, + Approved: response.Approved, + }); err != nil { + return err + } + for uuid := range response.Participants { + if uuid != response.ApprovingSite.SiteUUID { + go func(siteUUID string) { + participant := response.Participants[siteUUID] + sitePortalClient := siteportal.NewSitePortalClient(participant.ExternalHost, participant.ExternalPort, participant.HTTPS, participant.ServerName) + if err := sitePortalClient.SendJobApprovalResponse(response.JobUUID, siteportal.JobApprovalContext{ + SiteUUID: response.ApprovingSite.SiteUUID, + Approved: response.Approved, + }); err != nil { + log.Err(err).Str("job uuid", response.JobUUID).Str("site uuid", siteUUID).Msg("fail to send job approval to site") + } + }(uuid) + } + } + return nil +} + +// HandleJobStatusUpdate process job status update +func (s *JobService) HandleJobStatusUpdate(context *JobStatusUpdateContext) error { + for siteUUID, newInfo := range context.ParticipantStatusMap { + jobParticipantInstance, err := s.ParticipantRepo.GetByJobAndSiteUUID(context.JobUUID, siteUUID) + if err != nil { + log.Err(err).Str("participant_uuid", siteUUID).Msg("failed to get participant info") + continue + } + jobParticipant := jobParticipantInstance.(*entity.JobParticipant) + jobParticipant.Repo = s.ParticipantRepo + if jobParticipant.Status != newInfo.JobParticipantStatus { + if err := jobParticipant.UpdateStatus(newInfo.JobParticipantStatus); err != nil { + log.Err(err).Str("participant_uuid", siteUUID).Send() + } + } + go func(uuid string) { + client := siteportal.NewSitePortalClient(context.ParticipantStatusMap[uuid].ExternalHost, context.ParticipantStatusMap[uuid].ExternalPort, context.ParticipantStatusMap[uuid].HTTPS, context.ParticipantStatusMap[uuid].ServerName) + if err := client.SendJobStatusUpdate(context.JobUUID, context.RequestJson); err != nil { + log.Err(err).Str("job uuid", context.JobUUID).Str("site uuid", uuid).Msg("failed to send job status update") + } + }(siteUUID) + } + + jobInstance, err := s.JobRepo.GetByUUID(context.JobUUID) + if err != nil { + return errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + job.Repo = s.JobRepo + if err := job.Update(context.NewJobStatus); err != nil { + return errors.Wrap(err, "failed to update job") + } + return nil +} diff --git a/fml-manager/server/domain/service/project_service.go b/fml-manager/server/domain/service/project_service.go new file mode 100644 index 00000000..27fdfd1a --- /dev/null +++ b/fml-manager/server/domain/service/project_service.go @@ -0,0 +1,603 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "strings" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/siteportal" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// ProjectService is the service to handle project related requests +type ProjectService struct { + ProjectRepo repo.ProjectRepository + InvitationRepo repo.ProjectInvitationRepository + ParticipantRepo repo.ProjectParticipantRepository + ProjectDataRepo repo.ProjectDataRepository +} + +// ProjectInvitationRequest is an invitation for asking a site to join a project +type ProjectInvitationRequest struct { + InvitationUUID string + Project *entity.Project + ManagingSite *ProjectParticipantSiteInfo + TargetSite *ProjectParticipantSiteInfo + AssociatedData []entity.ProjectData +} + +// ProjectParticipantSiteInfo contains more detailed info of a participating site +type ProjectParticipantSiteInfo struct { + Name string + Description string + UUID string + PartyID uint + ExternalHost string + ExternalPort uint + HTTPS bool + ServerName string +} + +// HandleInvitationRequest creates/updates repo records and forward the invitation to the target site +func (s *ProjectService) HandleInvitationRequest(req *ProjectInvitationRequest) error { + // create project if needed + _, err := s.ProjectRepo.GetByUUID(req.Project.UUID) + if err != nil { + if errors.Is(err, repo.ErrProjectNotFound) { + log.Info().Msgf("creating project %s(%s)", req.Project.Name, req.Project.UUID) + req.Project.Model.ID = 0 + if err := s.ProjectRepo.Create(req.Project); err != nil { + // race condition + if !strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + return errors.Wrapf(err, "failed to create the project") + } else { + log.Info().Msgf("project %s(%s) already created, continue", req.Project.Name, req.Project.UUID) + } + } + if err := s.ParticipantRepo.Create(&entity.ProjectParticipant{ + Model: gorm.Model{}, + UUID: uuid.NewV4().String(), + ProjectUUID: req.Project.UUID, + SiteUUID: req.ManagingSite.UUID, + SiteName: req.ManagingSite.Name, + SitePartyID: req.ManagingSite.PartyID, + SiteDescription: req.ManagingSite.Description, + Status: entity.ProjectParticipantStatusOwner, + }); err != nil { + return errors.Wrapf(err, "failed to create the owner participant") + } + for _, data := range req.AssociatedData { + if err := s.createOrUpdateData(&data); err != nil { + return errors.Wrapf(err, "failed to create the owner data association for data(%s)", data.DataUUID) + } + } + } else { + return errors.Wrapf(err, "failed to query the project") + } + } + // create invitation + projectInvitation := &entity.ProjectInvitation{ + Model: gorm.Model{}, + UUID: req.InvitationUUID, + ProjectUUID: req.Project.UUID, + SiteUUID: req.TargetSite.UUID, + Status: entity.ProjectInvitationStatusCreated, + } + if err := s.InvitationRepo.Create(projectInvitation); err != nil { + return errors.Wrapf(err, "failed to create the invitation") + } + // create/update project participant info + instance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(req.Project.UUID, req.TargetSite.UUID) + if err != nil { + if errors.Is(err, repo.ErrProjectParticipantNotFound) { + projectParticipant := &entity.ProjectParticipant{ + Model: gorm.Model{}, + UUID: uuid.NewV4().String(), + ProjectUUID: req.Project.UUID, + SiteUUID: req.TargetSite.UUID, + SiteName: req.TargetSite.Name, + SitePartyID: req.TargetSite.PartyID, + SiteDescription: req.TargetSite.Description, + Status: entity.ProjectParticipantStatusPending, + } + if err := s.ParticipantRepo.Create(projectParticipant); err != nil { + return errors.Wrapf(err, "failed to create the target participant") + } + } else { + return errors.Wrapf(err, "failed to query the target participant") + } + } else { + participant := instance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusPending + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the target participant status") + } + } + // send invitation to site portal. this shouldn't be placed in a goroutine as we need to fail the original request if we hit error here + log.Info().Msgf("sending invitation to target site: %s(%s)", req.TargetSite.Name, req.TargetSite.UUID) + client := siteportal.NewSitePortalClient(req.TargetSite.ExternalHost, req.TargetSite.ExternalPort, req.TargetSite.HTTPS, req.TargetSite.ServerName) + if err := client.SendInvitation(&siteportal.ProjectInvitationRequest{ + UUID: projectInvitation.UUID, + SiteUUID: req.TargetSite.UUID, + SitePartyID: req.TargetSite.PartyID, + ProjectUUID: req.Project.UUID, + ProjectName: req.Project.Name, + ProjectDescription: req.Project.Description, + ProjectAutoApprovalEnabled: req.Project.AutoApprovalEnabled, + ProjectManager: req.Project.Manager, + ProjectManagingSiteName: req.Project.ManagingSiteName, + ProjectManagingSitePartyID: req.Project.ManagingSitePartyID, + ProjectManagingSiteUUID: req.Project.ManagingSiteUUID, + ProjectCreationTime: req.Project.CreatedAt, + }); err != nil { + return errors.Wrapf(err, "failed to forward the invitation") + } + // update invitation status + projectInvitation.Status = entity.ProjectInvitationStatusSent + if err := s.InvitationRepo.UpdateStatusByUUID(projectInvitation); err != nil { + return errors.Wrapf(err, "failed to update the invitation status") + } + return nil +} + +// HandleInvitationAcceptance updates the status in the DB and send the participants info to the joined sites +func (s *ProjectService) HandleInvitationAcceptance(req *ProjectInvitationRequest, otherSiteList []ProjectParticipantSiteInfo) error { + // sanity check + invitationInstance, err := s.InvitationRepo.GetByUUID(req.InvitationUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the invitation instance") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + + // update status + invitation.Status = entity.ProjectInvitationStatusAccepted + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(invitation.ProjectUUID, invitation.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the participant instance") + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusJoined + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the participant status") + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return errors.Wrapf(err, "failed to update the invitation status") + } + + go func() { + // send invitation acceptance to managing site + log.Info().Msgf("forwarding response to owner site: %s(%s)", req.ManagingSite.Name, req.ManagingSite.UUID) + client := siteportal.NewSitePortalClient(req.ManagingSite.ExternalHost, req.ManagingSite.ExternalPort, req.ManagingSite.HTTPS, req.ManagingSite.ServerName) + if err := client.SendInvitationAcceptance(req.InvitationUUID); err != nil { + log.Err(errors.Wrapf(err, "failed to redirect project invitation response")).Send() + } + + // send new participant to all joined site + for _, otherSite := range otherSiteList { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending new site info to site: %s(%s)", site.Name, site.UUID) + if site.UUID != req.TargetSite.UUID { + client = siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectParticipants(invitation.ProjectUUID, []siteportal.ProjectParticipant{ + { + UUID: participant.UUID, + ProjectUUID: participant.ProjectUUID, + SiteUUID: participant.SiteUUID, + SiteName: participant.SiteName, + SitePartyID: participant.SitePartyID, + SiteDescription: participant.SiteDescription, + Status: uint8(participant.Status), + }, + }); err != nil { + log.Err(err).Msgf("failed to send participants update to site: %s(%s), continue", site.Name, site.UUID) + } + } + }(otherSite) + } + + // send project participants to joining site + instanceList, err := s.ParticipantRepo.GetByProjectUUID(req.Project.UUID) + if err != nil { + log.Err(errors.Wrapf(err, "failed to get participant list")).Send() + return + } + participantList := instanceList.([]entity.ProjectParticipant) + var joinedParticipantList []siteportal.ProjectParticipant + for _, participant := range participantList { + if participant.Status == entity.ProjectParticipantStatusJoined || participant.Status == entity.ProjectParticipantStatusOwner { + joinedParticipantList = append(joinedParticipantList, siteportal.ProjectParticipant{ + UUID: participant.UUID, + ProjectUUID: participant.ProjectUUID, + SiteUUID: participant.SiteUUID, + SiteName: participant.SiteName, + SitePartyID: participant.SitePartyID, + SiteDescription: participant.SiteDescription, + Status: uint8(participant.Status), + }) + } + } + log.Info().Msgf("sending participants sites info to new site: %s(%s)", req.TargetSite.Name, req.TargetSite.UUID) + client = siteportal.NewSitePortalClient(req.TargetSite.ExternalHost, req.TargetSite.ExternalPort, req.TargetSite.HTTPS, req.TargetSite.ServerName) + if err := client.SendProjectParticipants(invitation.ProjectUUID, joinedParticipantList); err != nil { + log.Err(errors.Wrapf(err, "failed to send participant list to new site")).Send() + } + + // send associated data to the newly joined site + instanceList, err = s.ProjectDataRepo.GetListByProjectUUID(req.Project.UUID) + if err != nil { + log.Err(errors.Wrapf(err, "failed to get project data list")).Send() + return + } + dataList := instanceList.([]entity.ProjectData) + var associatedDataList []siteportal.ProjectData + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + associatedDataList = append(associatedDataList, siteportal.ProjectData{ + Name: data.Name, + Description: data.Description, + ProjectUUID: data.ProjectUUID, + DataUUID: data.DataUUID, + SiteUUID: data.SiteUUID, + SiteName: data.SiteName, + SitePartyID: data.SitePartyID, + TableName: data.TableName, + TableNamespace: data.TableNamespace, + CreationTime: data.CreationTime, + UpdateTime: data.UpdateTime, + }) + } + } + log.Info().Msgf("sending project data info to new site: %s(%s)", req.TargetSite.Name, req.TargetSite.UUID) + client = siteportal.NewSitePortalClient(req.TargetSite.ExternalHost, req.TargetSite.ExternalPort, req.TargetSite.HTTPS, req.TargetSite.ServerName) + if err := client.SendProjectDataAssociation(invitation.ProjectUUID, associatedDataList); err != nil { + log.Err(errors.Wrapf(err, "failed to send participant list to new site")).Send() + } + }() + return nil +} + +// HandleInvitationRejection updates the DB status and redirect the response to the owner site +func (s *ProjectService) HandleInvitationRejection(req *ProjectInvitationRequest) error { + invitationInstance, err := s.InvitationRepo.GetByUUID(req.InvitationUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the invitation instance") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRejected + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(invitation.ProjectUUID, invitation.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the participant instance") + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusRejected + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the participant status") + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return errors.Wrapf(err, "failed to update the invitation status") + } + // send rejection to owner site + go func() { + log.Info().Msgf("forwarding reject response to owner site: %s(%s)", req.ManagingSite.Name, req.ManagingSite.UUID) + client := siteportal.NewSitePortalClient(req.ManagingSite.ExternalHost, req.ManagingSite.ExternalPort, req.ManagingSite.HTTPS, req.ManagingSite.ServerName) + if err := client.SendInvitationRejection(req.InvitationUUID); err != nil { + log.Err(errors.Wrapf(err, "failed to redirect project invitation response")).Send() + } + }() + return nil +} + +// HandleInvitationRevocation updates the DB status and redirect the response to the target site +func (s *ProjectService) HandleInvitationRevocation(req *ProjectInvitationRequest) error { + invitationInstance, err := s.InvitationRepo.GetByUUID(req.InvitationUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the invitation instance") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRevoked + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(invitation.ProjectUUID, invitation.SiteUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the participant instance") + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusRevoked + + // send revocation to target site + log.Info().Msgf("sending invitation revocation to site: %s(%s)", req.TargetSite.Name, req.TargetSite.UUID) + client := siteportal.NewSitePortalClient(req.TargetSite.ExternalHost, req.TargetSite.ExternalPort, req.TargetSite.HTTPS, req.TargetSite.ServerName) + if err := client.SendInvitationRevocation(req.InvitationUUID); err != nil { + return errors.Wrapf(err, "failed to redirect project invitation response") + } + + // update DB records + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the participant status") + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return errors.Wrapf(err, "failed to update the invitation status") + } + return nil +} + +// HandleParticipantInfoUpdate updates the site info in the repo and send such update to impacted sites +func (s *ProjectService) HandleParticipantInfoUpdate(newSiteInfo ProjectParticipantSiteInfo, allSites []ProjectParticipantSiteInfo) error { + toUpdateProjectTemplate := &entity.Project{ + ProjectCreatorInfo: &valueobject.ProjectCreatorInfo{ + ManagingSiteName: newSiteInfo.Name, + ManagingSitePartyID: newSiteInfo.PartyID, + ManagingSiteUUID: newSiteInfo.UUID, + }, + } + if err := s.ProjectRepo.UpdateManagingSiteInfoBySiteUUID(toUpdateProjectTemplate); err != nil { + return errors.Wrapf(err, "failed to update projects creator info") + } + + toUpdateParticipantTemplate := &entity.ProjectParticipant{ + SiteUUID: newSiteInfo.UUID, + SiteName: newSiteInfo.Name, + SitePartyID: newSiteInfo.PartyID, + SiteDescription: newSiteInfo.Description, + } + if err := s.ParticipantRepo.UpdateParticipantInfoBySiteUUID(toUpdateParticipantTemplate); err != nil { + return errors.Wrapf(err, "failed to update projects participants info") + } + + toUpdateDataTemplate := &entity.ProjectData{ + SiteUUID: newSiteInfo.UUID, + SiteName: newSiteInfo.Name, + SitePartyID: newSiteInfo.PartyID, + } + if err := s.ProjectDataRepo.UpdateSiteInfoBySiteUUID(toUpdateDataTemplate); err != nil { + return errors.Wrap(err, "failed to update project data site info") + } + + go func() { + // XXX: we are issuing the event to all sites. Better to only issue the event to "impacted" sites + for _, targetSite := range allSites { + go func(site ProjectParticipantSiteInfo) { + if site.UUID == newSiteInfo.UUID { + return + } + log.Info().Msgf("sending participant info update event to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendParticipantInfoUpdateEvent(siteportal.ProjectParticipantUpdateEvent{ + UUID: newSiteInfo.UUID, + PartyID: newSiteInfo.PartyID, + Name: newSiteInfo.Name, + Description: newSiteInfo.Description, + }); err != nil { + log.Err(err).Msgf("failed to send site info update event to site: %s(%s)", site.Name, site.UUID) + } + }(targetSite) + } + }() + return nil +} + +// HandleParticipantLeaving updates the participant status in the repo and send such update to impacted sites +func (s *ProjectService) HandleParticipantLeaving(projectUUID, siteUUID string, otherSiteList []ProjectParticipantSiteInfo) error { + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to get the participant instance") + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusLeft + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the participant status") + } + // send such update to other sites + go func() { + for _, otherSite := range otherSiteList { + if otherSite.UUID != siteUUID { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending project participant leaving to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectParticipantLeaving(projectUUID, siteUUID); err != nil { + log.Err(err).Msgf("failed to send project participant leaving to site: %s(%s), continue", site.Name, site.UUID) + } + }(otherSite) + } + } + }() + return nil +} + +// HandleParticipantDismissal sends the dismissal to the target site and update the participant status in the repo and send such update to other sites +func (s *ProjectService) HandleParticipantDismissal(projectUUID string, targetSite ProjectParticipantSiteInfo, otherSiteList []ProjectParticipantSiteInfo) error { + // synchronously notify the target site, because maybe only the target site is running a job that no one is aware of + // TODO: provide a "force" option so that we can ignore error if the cause is that the target site is no longer exists + client := siteportal.NewSitePortalClient(targetSite.ExternalHost, targetSite.ExternalPort, targetSite.HTTPS, targetSite.ServerName) + if err := client.SendProjectParticipantDismissal(projectUUID, targetSite.UUID); err != nil { + return errors.Wrapf(err, "failed to send project participant dismissal to site: %s(%s)", targetSite.Name, targetSite.UUID) + } + // dismiss data association + dataListInstance, err := s.ProjectDataRepo.GetListByProjectAndSiteUUID(projectUUID, targetSite.UUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + data.Status = entity.ProjectDataStatusDismissed + if err := s.ProjectDataRepo.UpdateStatusByUUID(&data); err != nil { + return errors.Wrapf(err, "failed to dismiss data %s from site: %s", data.Name, targetSite.Name) + } + } + } + // update participant status + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, targetSite.UUID) + if err != nil { + return errors.Wrapf(err, "failed to get the participant instance") + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusDismissed + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update the participant status") + } + // send such event to other sites + go func() { + for _, otherSite := range otherSiteList { + if otherSite.UUID != targetSite.UUID { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending project participant dismissal to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectParticipantDismissal(projectUUID, targetSite.UUID); err != nil { + log.Err(err).Msgf("failed to send project participant leaving to site: %s(%s), continue", site.Name, site.UUID) + } + }(otherSite) + } + } + }() + return nil +} + +// HandleDataAssociation sends the new data association to other participating sites +func (s *ProjectService) HandleDataAssociation(newData *entity.ProjectData, otherSiteList []ProjectParticipantSiteInfo) error { + if err := s.createOrUpdateData(newData); err != nil { + return err + } + // inform other joined site of this newly associated data + go func() { + for _, otherSite := range otherSiteList { + if otherSite.UUID != newData.SiteUUID { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending new project data info to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectDataAssociation(newData.ProjectUUID, []siteportal.ProjectData{ + { + Name: newData.Name, + Description: newData.Description, + ProjectUUID: newData.ProjectUUID, + DataUUID: newData.DataUUID, + SiteUUID: newData.SiteUUID, + SiteName: newData.SiteName, + SitePartyID: newData.SitePartyID, + TableName: newData.TableName, + TableNamespace: newData.TableNamespace, + CreationTime: newData.CreationTime, + UpdateTime: newData.UpdateTime, + }, + }); err != nil { + log.Err(err).Msgf("failed to send new project data info to site: %s(%s), continue", site.Name, site.UUID) + } + }(otherSite) + } + } + }() + return nil +} + +// HandleDataDismissal sends data dismissal event to other participating site +func (s *ProjectService) HandleDataDismissal(projectUUID, dataUUID string, otherSiteList []ProjectParticipantSiteInfo) error { + providingSiteUUID := "" + instance, err := s.ProjectDataRepo.GetByProjectAndDataUUID(projectUUID, dataUUID) + if err != nil { + if errors.Is(err, repo.ErrProjectDataNotFound) { + log.Warn().Str("data uuid", dataUUID).Str("project uuid", projectUUID).Msg("data not associated in this project") + } else { + return errors.Wrapf(err, "failed to query data association") + } + } else { + data := instance.(*entity.ProjectData) + data.Status = entity.ProjectDataStatusDismissed + providingSiteUUID = data.SiteUUID + if err := s.ProjectDataRepo.UpdateStatusByUUID(data); err != nil { + return errors.Wrapf(err, "failed to update data association") + } + } + // inform other joined site of this dismissed associated data + go func() { + for _, otherSite := range otherSiteList { + if otherSite.UUID != providingSiteUUID { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending project data dismissal to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectDataDismissal(projectUUID, []string{dataUUID}); err != nil { + log.Err(err).Msgf("failed to send project data dismissal to site: %s(%s), continue", site.Name, site.UUID) + } + }(otherSite) + } + } + }() + + return nil +} + +// createOrUpdateData creates or updates the project data records +func (s *ProjectService) createOrUpdateData(newData *entity.ProjectData) error { + instance, err := s.ProjectDataRepo.GetByProjectAndDataUUID(newData.ProjectUUID, newData.DataUUID) + if err != nil { + if errors.Is(err, repo.ErrProjectDataNotFound) { + newData.Model.ID = 0 + newData.UUID = uuid.NewV4().String() + newData.Status = entity.ProjectDataStatusAssociated + if err := s.ProjectDataRepo.Create(newData); err != nil { + return errors.Wrapf(err, "failed to create data association") + } + } else { + return errors.Wrapf(err, "failed to query data association") + } + } else { + data := instance.(*entity.ProjectData) + data.Status = entity.ProjectDataStatusAssociated + if err := s.ProjectDataRepo.UpdateStatusByUUID(data); err != nil { + return errors.Wrapf(err, "failed to update data association") + } + } + return nil +} + +// HandleProjectClosing updates project status and sends the event to other site +func (s *ProjectService) HandleProjectClosing(projectUUID string, otherSiteList []ProjectParticipantSiteInfo) error { + + projectInstance, err := s.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrapf(err, "failed to find project") + } + project := projectInstance.(*entity.Project) + + project.Status = entity.ProjectStatusClosed + if err := s.ProjectRepo.UpdateStatusByUUID(project); err != nil { + return errors.Wrapf(err, "failed to update project status") + } + + go func() { + for _, otherSite := range otherSiteList { + go func(site ProjectParticipantSiteInfo) { + log.Info().Msgf("sending project closing to site: %s(%s)", site.Name, site.UUID) + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + if err := client.SendProjectClosing(projectUUID); err != nil { + log.Err(err).Msgf("failed to send project closing to site: %s(%s), continue", site.Name, site.UUID) + } + }(otherSite) + } + }() + return nil +} diff --git a/fml-manager/server/domain/service/site_service.go b/fml-manager/server/domain/service/site_service.go new file mode 100644 index 00000000..bde52cf5 --- /dev/null +++ b/fml-manager/server/domain/service/site_service.go @@ -0,0 +1,80 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/event" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/siteportal" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +type SiteService struct { + // SiteRepo is the repository for persisting site info + SiteRepo repo.SiteRepository +} + +// HandleSiteRegistration creates or updates the site info +func (s *SiteService) HandleSiteRegistration(site *entity.Site) error { + if site.UUID == "" { + return errors.New("invalid uuid") + } + if site.ExternalHost == "" || site.ExternalPort == 0 { + return errors.New("invalid site connection info") + } + if site.Name == "" { + return errors.New("invalid site name") + } + // check the connection with site + client := siteportal.NewSitePortalClient(site.ExternalHost, site.ExternalPort, site.HTTPS, site.ServerName) + err := client.CheckSiteStatus() + if err != nil { + return errors.Wrapf(err, "fml manager can not connect to site") + } + // reset the gorm.Model fields + site.Model = gorm.Model{} + site.LastRegisteredAt = time.Now() + exist, err := s.SiteRepo.ExistByUUID(site.UUID) + if err != nil { + return errors.Wrap(err, "failed to find site info") + } + if exist { + log.Info().Msgf("deleting stale site info with uuid: %s", site.UUID) + if err := s.SiteRepo.DeleteByUUID(site.UUID); err != nil { + return err + } + } + log.Info().Msgf("creating site: %s with uuid: %s", site.Name, site.UUID) + _, err = s.SiteRepo.Save(site) + + // send the site info update event to the project context + go func() { + log.Info().Msgf("sending site info update event to project context: site %s(%s)", site.Name, site.UUID) + if err := event.NewSelfHttpExchange().PostEvent(event.ProjectParticipantUpdateEvent{ + UUID: site.UUID, + PartyID: site.PartyID, + Name: site.Name, + Description: site.Description, + }); err != nil { + log.Err(err).Msgf("failed to post site info update event") + } + }() + return err +} diff --git a/fml-manager/server/domain/valueobject/project_creator_info.go b/fml-manager/server/domain/valueobject/project_creator_info.go new file mode 100644 index 00000000..b19fb5d4 --- /dev/null +++ b/fml-manager/server/domain/valueobject/project_creator_info.go @@ -0,0 +1,23 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +// ProjectCreatorInfo contains info of the creator/manager of a project +type ProjectCreatorInfo struct { + Manager string `json:"manager" gorm:"type:varchar(255)"` + ManagingSiteName string `json:"managing_site_name" gorm:"type:varchar(255)"` + ManagingSitePartyID uint `json:"managing_site_party_id"` + ManagingSiteUUID string `json:"managing_site_uuid" gorm:"type:varchar(36)"` +} diff --git a/fml-manager/server/infrastructure/event/exchange.go b/fml-manager/server/infrastructure/event/exchange.go new file mode 100644 index 00000000..33a487c0 --- /dev/null +++ b/fml-manager/server/infrastructure/event/exchange.go @@ -0,0 +1,137 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package event + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// Exchange handles event posting +type Exchange interface { + // PostEvent sends event + PostEvent(event Event) error +} + +type selfHttpExchange struct { + endpoint string +} + +// NewSelfHttpExchange returns an Exchange for posting event to this service itself +func NewSelfHttpExchange() Exchange { + tlsEnabled := viper.GetBool("fmlmanager.tls.enabled") + if tlsEnabled { + tlsPort := viper.GetString("fmlmanager.tls.port") + if tlsPort == "" { + tlsPort = "8443" + } + return &selfHttpExchange{ + endpoint: fmt.Sprintf("https://localhost:%s", tlsPort), + } + } + // gin's default port + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + return &selfHttpExchange{ + endpoint: fmt.Sprintf("http://localhost:%s", port), + } +} + +func (e *selfHttpExchange) PostEvent(event Event) error { + urlStr := e.genURL(event.GetUrl()) + var payload []byte + payload, err := json.Marshal(event) + if err != nil { + return err + } + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + u, err := url.Parse(urlStr) + if err != nil { + return errors.Wrap(err, "Parse URL failed") + } + var resp *http.Response + //if endpoint scheme is https, use tls + if u.Scheme == "https" { + caCertPath := viper.GetString("fmlmanager.tls.ca.cert") + fmlManagerClientCert := viper.GetString("fmlmanager.tls.client.cert") + fmlManagerClientKey := viper.GetString("fmlmanager.tls.client.key") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + return errors.Wrapf(err, "read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + clientCrt, err := tls.LoadX509KeyPair(fmlManagerClientCert, fmlManagerClientKey) + if err != nil { + return errors.Wrapf(err, "LoadX509KeyPair error:") + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{clientCrt}, + }, + } + client := &http.Client{Transport: tr} + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTPs with body %s", urlStr, string(payload))) + resp, err = client.Do(req) + if err != nil { + return err + } + } else { + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTP with body %s", urlStr, string(payload))) + resp, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + } + defer resp.Body.Close() + _, err = e.parseResponse(resp) + return err +} + +func (e *selfHttpExchange) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return nil, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + return nil, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} + +func (e *selfHttpExchange) genURL(path string) string { + return fmt.Sprintf("%s/api/v1/%s", e.endpoint, path) +} diff --git a/fml-manager/server/infrastructure/event/types.go b/fml-manager/server/infrastructure/event/types.go new file mode 100644 index 00000000..92d55cfa --- /dev/null +++ b/fml-manager/server/infrastructure/event/types.go @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package event + +// Event is an interface representing an event to be handled +type Event interface { + // GetUrl returns the base path of the event + GetUrl() string +} + +// ProjectParticipantUpdateEvent is triggered when a site info is updated +type ProjectParticipantUpdateEvent struct { + UUID string `json:"uuid"` + PartyID uint `json:"party_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func (e ProjectParticipantUpdateEvent) GetUrl() string { + return "project/event/participant/update" +} diff --git a/fml-manager/server/infrastructure/gorm/database.go b/fml-manager/server/infrastructure/gorm/database.go new file mode 100644 index 00000000..6feb5830 --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/database.go @@ -0,0 +1,73 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/spf13/viper" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +const ( + dsnTemplate = "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=Asia/Shanghai" +) + +// InitDB initialize the connection to the PostgresSql db +func InitDB() error { + host := viper.GetString("postgres.host") + port := viper.GetString("postgres.port") + dbName := viper.GetString("postgres.db") + user := viper.GetString("postgres.user") + password := viper.GetString("postgres.password") + if host == "" || port == "" || dbName == "" || user == "" || password == "" { + panic("database information incomplete") + } + + sslMode := viper.GetString("postgres.sslmode") + if sslMode == "" { + sslMode = "disable" + } + + debugLog, _ := strconv.ParseBool(viper.GetString("postgres.debug")) + loglevel := logger.Silent + if debugLog { + loglevel = logger.Info + } + + dsn := fmt.Sprintf(dsnTemplate, host, port, user, password, dbName, sslMode) + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: loglevel, + Colorful: false, + }, + ) + if _db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: newLogger}); err != nil { + return err + } else { + db = _db + } + return nil +} diff --git a/fml-manager/server/infrastructure/gorm/job_participant_repo.go b/fml-manager/server/infrastructure/gorm/job_participant_repo.go new file mode 100644 index 00000000..634f46c7 --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/job_participant_repo.go @@ -0,0 +1,80 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// JobParticipantRepo implements repo.JobParticipantRepository using gorm and PostgreSQL +type JobParticipantRepo struct{} + +// make sure JobParticipantRepo implements the repo.JobParticipantRepository interface +var _ repo.JobParticipantRepository = (*JobParticipantRepo)(nil) + +func (r *JobParticipantRepo) Create(instance interface{}) error { + newJobParticipant := instance.(*entity.JobParticipant) + return db.Model(&entity.JobParticipant{}).Create(newJobParticipant).Error +} + +func (r *JobParticipantRepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.JobParticipant) + return db.Model(&entity.JobParticipant{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *JobParticipantRepo) GetStatusByUUID(instance interface{}) interface{} { + participant := instance.(*entity.JobParticipant) + var status int + row := db.Model(&entity.JobParticipant{}).Where("uuid = ?", participant.UUID).Select("status", status).Row() + err := row.Scan(&status) + if err != nil { + log.Info().Msgf("Do not find any status for job-participant uuid %s, err msg %s", participant.UUID, err.Error()) + return entity.JobParticipantStatusUnknown + } + return status +} + +func (r *JobParticipantRepo) GetByJobAndSiteUUID(jobUUID, siteUUID string) (interface{}, error) { + participant := &entity.JobParticipant{} + if err := db.Model(&entity.JobParticipant{}). + Where("job_uuid = ? AND site_uuid = ?", jobUUID, siteUUID). + First(participant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrJobParticipantNotFound + } + return nil, err + } + return participant, nil +} + +func (r *JobParticipantRepo) GetListByJobUUID(jobUUID string) (interface{}, error) { + var participantList []entity.JobParticipant + if err := db.Where("job_uuid = ?", jobUUID).Find(&participantList).Error; err != nil { + return nil, err + } + return participantList, nil +} + +// InitTable make sure the table is created in the db +func (r *JobParticipantRepo) InitTable() { + if err := db.AutoMigrate(&entity.JobParticipant{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/job_repo.go b/fml-manager/server/infrastructure/gorm/job_repo.go new file mode 100644 index 00000000..d86b15fc --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/job_repo.go @@ -0,0 +1,119 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// JobRepo implements repo.JobRepository using gorm and PostgreSQL +type JobRepo struct{} + +// make sure JobRepo implements the repo.JobRepository interface +var _ repo.JobRepository = (*JobRepo)(nil) + +var ErrJobNameConflict = errors.New("job name conflicts") + +func (r *JobRepo) Create(instance interface{}) error { + newJob := instance.(*entity.Job) + // XXX: decide whether we should allow name conflict + return db.Model(&entity.Job{}).Create(newJob).Error +} + +func (r *JobRepo) UpdateFATEJobInfoByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Select("fate_job_id", "fate_job_status", "fate_model_id", "fate_model_version").Updates(job).Error +} + +func (r *JobRepo) UpdateFATEJobStatusByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("fate_job_status", job.FATEJobStatus).Error +} + +func (r *JobRepo) UpdateStatusByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("status", job.Status).Error +} + +func (r *JobRepo) UpdateStatusMessageByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("status_message", job.StatusMessage).Error +} + +func (r *JobRepo) UpdateFinishTimeByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("finished_at", job.FinishedAt).Error +} + +func (r *JobRepo) CheckNameConflict(name string) error { + var count int64 + err := db.Model(&entity.Job{}). + Where("name = ?", name). + Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return ErrJobNameConflict + } + return nil +} + +func (r *JobRepo) DeleteByProjectUUID(projectUUID string) error { + return db.Unscoped().Where("project_uuid = ?", projectUUID).Delete(&entity.Job{}).Error +} + +func (r *JobRepo) GetAll() (interface{}, error) { + var jobList []entity.Job + if err := db.Find(&jobList).Error; err != nil { + return nil, err + } + return jobList, nil +} + +func (r *JobRepo) GetListByProjectUUID(projectUUID string) (interface{}, error) { + var jobList []entity.Job + err := db.Where("project_uuid = ?", projectUUID).Find(&jobList).Error + if err != nil { + return 0, err + } + return jobList, nil +} + +func (r *JobRepo) GetByUUID(uuid string) (interface{}, error) { + job := &entity.Job{} + if err := db.Where("uuid = ?", uuid).First(job).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrJobNotFound + } + return nil, err + } + return job, nil +} + +// InitTable make sure the table is created in the db +func (r *JobRepo) InitTable() { + if err := db.AutoMigrate(&entity.Job{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/project_data_repo.go b/fml-manager/server/infrastructure/gorm/project_data_repo.go new file mode 100644 index 00000000..9384b68f --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/project_data_repo.go @@ -0,0 +1,90 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectDataRepo implements repo.ProjectDataRepository +type ProjectDataRepo struct{} + +var _ repo.ProjectDataRepository = (*ProjectDataRepo)(nil) + +func (r *ProjectDataRepo) Create(instance interface{}) error { + newData := instance.(*entity.ProjectData) + return db.Model(&entity.ProjectData{}).Create(newData).Error +} + +func (r *ProjectDataRepo) GetByProjectAndDataUUID(projectUUID string, dataUUID string) (interface{}, error) { + data := &entity.ProjectData{} + if err := db.Model(&entity.ProjectData{}). + Where("project_uuid = ? AND data_uuid = ?", projectUUID, dataUUID). + First(data).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectDataNotFound + } + return nil, err + } + return data, nil +} + +func (r *ProjectDataRepo) UpdateStatusByUUID(instance interface{}) error { + data := instance.(*entity.ProjectData) + return db.Model(&entity.ProjectData{}).Where("uuid = ?", data.UUID). + Update("status", data.Status).Error +} + +func (r *ProjectDataRepo) GetListByProjectUUID(projectUUID string) (interface{}, error) { + var projectDataList []entity.ProjectData + err := db.Where("project_uuid = ?", projectUUID).Find(&projectDataList).Error + if err != nil { + return 0, err + } + return projectDataList, nil +} + +func (r *ProjectDataRepo) GetListByProjectAndSiteUUID(projectUUID string, siteUUID string) (interface{}, error) { + var projectDataList []entity.ProjectData + err := db.Where("project_uuid = ? AND site_uuid = ?", projectUUID, siteUUID).Find(&projectDataList).Error + if err != nil { + return 0, err + } + return projectDataList, nil +} + +func (r *ProjectDataRepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.ProjectData{}).Error +} + +func (r *ProjectDataRepo) DeleteByProjectUUID(projectUUID string) error { + return db.Unscoped().Where("project_uuid = ?", projectUUID).Delete(&entity.ProjectData{}).Error +} + +func (r *ProjectDataRepo) UpdateSiteInfoBySiteUUID(instance interface{}) error { + site := instance.(*entity.ProjectData) + return db.Where("site_uuid = ?", site.SiteUUID). + Select("site_name", "site_party_id").Updates(site).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectDataRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectData{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/project_invitation_repo.go b/fml-manager/server/infrastructure/gorm/project_invitation_repo.go new file mode 100644 index 00000000..5be5021b --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/project_invitation_repo.go @@ -0,0 +1,61 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" +) + +// ProjectInvitationRepo implements repo.ProjectInvitationRepository +type ProjectInvitationRepo struct{} + +var _ repo.ProjectInvitationRepository = (*ProjectInvitationRepo)(nil) + +func (r *ProjectInvitationRepo) Create(instance interface{}) error { + newInvitation := instance.(*entity.ProjectInvitation) + return db.Model(&entity.ProjectInvitation{}).Create(newInvitation).Error +} + +func (r *ProjectInvitationRepo) UpdateStatusByUUID(instance interface{}) error { + invitation := instance.(*entity.ProjectInvitation) + return db.Model(&entity.ProjectInvitation{}).Where("uuid = ?", invitation.UUID). + Update("status", invitation.Status).Error +} + +func (r *ProjectInvitationRepo) GetByProjectUUID(uuid string) (interface{}, error) { + invitation := &entity.ProjectInvitation{} + if err := db.Model(&entity.ProjectInvitation{}).Where("project_uuid = ?", uuid). + Last(invitation).Error; err != nil { + return nil, err + } + return invitation, nil +} + +func (r *ProjectInvitationRepo) GetByUUID(uuid string) (interface{}, error) { + invitation := &entity.ProjectInvitation{} + if err := db.Model(&entity.ProjectInvitation{}).Where("uuid = ?", uuid). + First(invitation).Error; err != nil { + return nil, err + } + return invitation, nil +} + +// InitTable make sure the table is created in the db +func (r *ProjectInvitationRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectInvitation{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/project_participant_repo.go b/fml-manager/server/infrastructure/gorm/project_participant_repo.go new file mode 100644 index 00000000..0de55e9e --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/project_participant_repo.go @@ -0,0 +1,82 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectParticipantRepo implements repo.ProjectParticipantRepository +type ProjectParticipantRepo struct{} + +var _ repo.ProjectParticipantRepository = (*ProjectParticipantRepo)(nil) + +func (r *ProjectParticipantRepo) GetByProjectUUID(uuid string) (interface{}, error) { + var participantList []entity.ProjectParticipant + err := db.Where("project_uuid = ?", uuid).Find(&participantList).Error + if err != nil { + return 0, err + } + return participantList, nil +} + +func (r *ProjectParticipantRepo) GetBySiteUUID(siteUUID string) (interface{}, error) { + var participantList []entity.ProjectParticipant + err := db.Where("site_uuid = ?", siteUUID).Find(&participantList).Error + if err != nil { + return 0, err + } + return participantList, nil +} + +func (r *ProjectParticipantRepo) GetByProjectAndSiteUUID(projectUUID, siteUUID string) (interface{}, error) { + participant := &entity.ProjectParticipant{} + if err := db.Model(&entity.ProjectParticipant{}). + Where("project_uuid = ? AND site_uuid = ?", projectUUID, siteUUID). + First(participant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectParticipantNotFound + } + return nil, err + } + return participant, nil +} + +func (r *ProjectParticipantRepo) Create(instance interface{}) error { + newParticipant := instance.(*entity.ProjectParticipant) + return db.Model(&entity.ProjectParticipant{}).Create(newParticipant).Error +} + +func (r *ProjectParticipantRepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.ProjectParticipant) + return db.Model(&entity.ProjectParticipant{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *ProjectParticipantRepo) UpdateParticipantInfoBySiteUUID(instance interface{}) error { + participant := instance.(*entity.ProjectParticipant) + return db.Model(&entity.ProjectParticipant{}).Where("site_uuid = ?", participant.SiteUUID). + Select("site_name", "site_party_id", "site_description").Updates(participant).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectParticipantRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectParticipant{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/project_repo.go b/fml-manager/server/infrastructure/gorm/project_repo.go new file mode 100644 index 00000000..6bd567ce --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/project_repo.go @@ -0,0 +1,70 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectRepo implements repo.ProjectRepository +type ProjectRepo struct{} + +var _ repo.ProjectRepository = (*ProjectRepo)(nil) + +func (r *ProjectRepo) Create(instance interface{}) error { + newProject := instance.(*entity.Project) + return db.Model(&entity.Project{}).Create(newProject).Error +} + +func (r *ProjectRepo) GetAll() (interface{}, error) { + var projectList []entity.Project + if err := db.Find(&projectList).Error; err != nil { + return nil, err + } + return projectList, nil +} + +func (r *ProjectRepo) GetByUUID(uuid string) (interface{}, error) { + project := &entity.Project{} + if err := db.Where("uuid = ?", uuid).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectNotFound + } + return nil, err + } + return project, nil +} + +func (r *ProjectRepo) UpdateManagingSiteInfoBySiteUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("managing_site_uuid = ?", project.ManagingSiteUUID). + Select("managing_site_name", "managing_site_party_id").Updates(project).Error +} + +func (r *ProjectRepo) UpdateStatusByUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("uuid = ?", project.UUID). + Update("status", project.Status).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectRepo) InitTable() { + if err := db.AutoMigrate(&entity.Project{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/gorm/site_repo.go b/fml-manager/server/infrastructure/gorm/site_repo.go new file mode 100644 index 00000000..fad139bf --- /dev/null +++ b/fml-manager/server/infrastructure/gorm/site_repo.go @@ -0,0 +1,88 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/entity" + "github.com/FederatedAI/FedLCM/fml-manager/server/domain/repo" +) + +// SiteRepo is the implementation of the domain's repo interface +type SiteRepo struct{} + +// make sure UserRepo implements the repo.UserRepository interface +var _ repo.SiteRepository = (*SiteRepo)(nil) + +// GetSiteList returns all the saved sites +func (r *SiteRepo) GetSiteList() (interface{}, error) { + var sites []entity.Site + if err := db.Find(&sites).Error; err != nil { + return nil, err + } + return sites, nil +} + +// Save create a record of site in the DB +func (r *SiteRepo) Save(instance interface{}) (interface{}, error) { + site := instance.(*entity.Site) + + var count int64 + db.Model(&entity.Site{}).Where("name = ?", site.Name).Count(&count) + if count > 0 { + return nil, repo.ErrSiteNameConflict + } + + if err := db.Model(&entity.Site{}).Create(site).Error; err != nil { + return nil, err + } + return site, nil +} + +// ExistByUUID returns whether the site with the uuid exists +func (r *SiteRepo) ExistByUUID(uuid string) (bool, error) { + var count int64 + err := db.Model(&entity.Site{}).Unscoped().Where("uuid = ?", uuid).Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// UpdateByUUID updates the site info indexed by the uuid +func (r *SiteRepo) UpdateByUUID(instance interface{}) error { + updatedSite := instance.(*entity.Site) + return db.Model(&entity.Site{}).Where("uuid = ?", updatedSite.UUID).Updates(updatedSite).Error +} + +// DeleteByUUID deletes records using the specified uuid +func (r *SiteRepo) DeleteByUUID(uuid string) error { + return db.Model(&entity.Site{}).Unscoped().Where("uuid = ?", uuid).Delete(&entity.Site{}).Error +} + +// GetByUUID returns an *entity.Site indexed by the uuid +func (r *SiteRepo) GetByUUID(uuid string) (interface{}, error) { + site := &entity.Site{} + if err := db.Model(&entity.Site{}).Where("uuid = ?", uuid).First(site).Error; err != nil { + return nil, err + } + return site, nil +} + +// InitTable make sure the table is created in the db +func (r *SiteRepo) InitTable() { + if err := db.AutoMigrate(entity.Site{}); err != nil { + panic(err) + } +} diff --git a/fml-manager/server/infrastructure/siteportal/client.go b/fml-manager/server/infrastructure/siteportal/client.go new file mode 100644 index 00000000..2f54d1c7 --- /dev/null +++ b/fml-manager/server/infrastructure/siteportal/client.go @@ -0,0 +1,337 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package siteportal + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// Client is an interface to work with certain site portal service +type Client interface { + // SendInvitation sends a project invitation to a target site + SendInvitation(request *ProjectInvitationRequest) error + // SendInvitationAcceptance sends invitation acceptance response to the site + SendInvitationAcceptance(invitationUUID string) error + // SendInvitationRejection sends invitation rejection to the site + SendInvitationRejection(invitationUUID string) error + // SendProjectParticipants sends a list of ProjectParticipant to the site + SendProjectParticipants(projectUUID string, participants []ProjectParticipant) error + // SendInvitationRevocation sends invitation revocation response to the site + SendInvitationRevocation(invitationUUID string) error + // SendParticipantInfoUpdateEvent sends site info update event to the site + SendParticipantInfoUpdateEvent(event ProjectParticipantUpdateEvent) error + // SendProjectDataAssociation sends new data association to the site + SendProjectDataAssociation(projectUUID string, data []ProjectData) error + // SendProjectDataDismissal sends data association dismissal to the site + SendProjectDataDismissal(projectUUID string, data []string) error + // SendJobCreationRequest asks the site portal to create a new job + SendJobCreationRequest(request string) error + // SendJobApprovalResponse sends the approval result of a job to the initiating site + SendJobApprovalResponse(jobUUID string, context JobApprovalContext) error + // SendJobStatusUpdate sends the job status update + SendJobStatusUpdate(jobUUID string, context string) error + // SendProjectParticipantLeaving sends the participant leaving event + SendProjectParticipantLeaving(projectUUID string, siteUUID string) error + // SendProjectParticipantDismissal sends the participant dismissal event + SendProjectParticipantDismissal(projectUUID string, siteUUID string) error + // SendProjectClosing sends the project closing event + SendProjectClosing(projectUUID string) error + // CheckSiteStatus checks the status of the site + CheckSiteStatus() error +} + +// NewSitePortalClient returns a site port Client instance +func NewSitePortalClient(host string, port uint, https bool, serverName string) Client { + scheme := "http" + if https { + scheme += "s" + } + return &client{ + endpoint: fmt.Sprintf("%s://%s:%d", scheme, host, port), + serverName: serverName, + } +} + +type client struct { + endpoint string + serverName string +} + +func (c *client) SendInvitation(request *ProjectInvitationRequest) error { + resp, err := c.postJSON("project/internal/invitation", request) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendInvitationAcceptance(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/invitation/%s/accept", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendInvitationRejection(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/invitation/%s/reject", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendInvitationRevocation(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/invitation/%s/revoke", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectParticipants(projectUUID string, participants []ProjectParticipant) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/participants", projectUUID), participants) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendParticipantInfoUpdateEvent(event ProjectParticipantUpdateEvent) error { + resp, err := c.postJSON("project/internal/event/participant/update", event) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectParticipantLeaving(projectUUID, siteUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/participant/%s/leave", projectUUID, siteUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectParticipantDismissal(projectUUID, siteUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/participant/%s/dismiss", projectUUID, siteUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectDataAssociation(projectUUID string, data []ProjectData) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/data/associate", projectUUID), data) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectDataDismissal(projectUUID string, data []string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/data/dismiss", projectUUID), data) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendJobCreationRequest(request string) error { + resp, err := c.postJSON("job/internal/create", request) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendJobApprovalResponse(jobUUID string, context JobApprovalContext) error { + resp, err := c.postJSON(fmt.Sprintf("job/internal/%s/response", jobUUID), context) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendJobStatusUpdate(jobUUID string, context string) error { + resp, err := c.postJSON(fmt.Sprintf("job/internal/%s/status", jobUUID), context) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) SendProjectClosing(projectUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/internal/%s/close", projectUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +func (c *client) postJSON(path string, body interface{}) (*http.Response, error) { + urlStr := c.genURL(path) + var payload []byte + if stringBody, ok := body.(string); ok { + payload = []byte(stringBody) + } else { + var err error + if payload, err = json.Marshal(body); err != nil { + return nil, err + } + } + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + u, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Wrap(err, "parse URL failed") + } + //if endpoint scheme is https, use tls + if u.Scheme == "https" { + client, err := c.getHttpsClientWithCert() + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTPS with body %s", urlStr, string(payload))) + return client.Do(req) + } else { + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTP with body %s", urlStr, string(payload))) + return http.DefaultClient.Do(req) + } +} + +func (c *client) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return nil, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + return nil, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} + +func (c *client) genURL(path string) string { + return fmt.Sprintf("%s/api/v1/%s", c.endpoint, path) +} + +func (c *client) CheckSiteStatus() error { + urlStr := c.genURL("status") + var resp *http.Response + u, err := url.Parse(urlStr) + if err != nil { + return errors.Wrap(err, "Parse URL failed") + } + if u.Scheme == "https" { + client, err := c.getHttpsClientWithCert() + if err != nil { + return err + } + resp, err = client.Get(urlStr) + if err != nil { + return err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTPs", urlStr)) + } else { + resp, err = http.Get(urlStr) + if err != nil { + return err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTP", urlStr)) + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return err + } + m := make(map[string]interface{}) + if err := json.Unmarshal(body, &m); err != nil { + return err + } + if m["msg"] != "The service is running" { + return errors.New("The service is not running") + } + return nil +} + +// getHttpsClientWithCert returns a https client use fml manager client cert +func (c *client) getHttpsClientWithCert() (*http.Client, error) { + caCertPath := viper.GetString("fmlmanager.tls.ca.cert") + fmlManagerClientCert := viper.GetString("fmlmanager.tls.client.cert") + fmlManagerClientKey := viper.GetString("fmlmanager.tls.client.key") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + return nil, errors.Wrapf(err, "read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + clientCrt, err := tls.LoadX509KeyPair(fmlManagerClientCert, fmlManagerClientKey) + if err != nil { + return nil, errors.Wrapf(err, "LoadX509KeyPair error:") + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{clientCrt}, + ServerName: c.serverName, + }, + } + return &http.Client{Transport: tr}, nil +} diff --git a/fml-manager/server/infrastructure/siteportal/type.go b/fml-manager/server/infrastructure/siteportal/type.go new file mode 100644 index 00000000..3f1a60e6 --- /dev/null +++ b/fml-manager/server/infrastructure/siteportal/type.go @@ -0,0 +1,86 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package siteportal + +import ( + "time" +) + +// ProjectInvitationRequest is an invitation for asking a site to join a project +type ProjectInvitationRequest struct { + UUID string `json:"uuid"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` +} + +// ProjectParticipant represents a site in a project +type ProjectParticipant struct { + UUID string `json:"uuid"` + ProjectUUID string `json:"project_uuid"` + SiteUUID string `json:"site_uuid"` + SiteName string `json:"site_name"` + SitePartyID uint `json:"site_party_id"` + SiteDescription string `json:"site_description"` + Status uint8 `json:"status"` +} + +// ProjectData represents a data association in a project +type ProjectData struct { + Name string `json:"name"` + Description string `json:"description"` + ProjectUUID string `json:"project_uuid"` + DataUUID string `json:"data_uuid"` + SiteUUID string `json:"site_uuid"` + SiteName string `json:"site_name"` + SitePartyID uint `json:"site_party_id"` + TableName string `json:"table_name"` + TableNamespace string `json:"table_namespace"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ProjectParticipantUpdateEvent represents a site info update event +type ProjectParticipantUpdateEvent struct { + UUID string `json:"uuid"` + PartyID uint `json:"party_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// JobApprovalContext is the struct containing job approval response +type JobApprovalContext struct { + SiteUUID string `json:"site_uuid"` + Approved bool `json:"approved"` +} + +// JobStatusUpdateContext contains info of the updated job status +type JobStatusUpdateContext struct { + Status uint8 `json:"status"` + StatusMessage string `json:"status_message"` + FATEJobID string `json:"fate_job_id"` + FATEJobStatus string `json:"fate_job_status"` + FATEModelID string `json:"fate_model_id"` + FATEModelVersion string `json:"fate_model_version"` + ParticipantStatusMap map[string]uint8 `json:"participant_status_map"` +} diff --git a/fml-manager/server/main.go b/fml-manager/server/main.go new file mode 100644 index 00000000..f2cdf383 --- /dev/null +++ b/fml-manager/server/main.go @@ -0,0 +1,177 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/fml-manager/server/api" + "github.com/FederatedAI/FedLCM/fml-manager/server/constants" + "github.com/FederatedAI/FedLCM/fml-manager/server/infrastructure/gorm" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/utils/logging" + "github.com/gin-contrib/logger" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + // swag API + _ "github.com/FederatedAI/FedLCM/fml-manager/server/docs" +) + +// main starts the API server +// @title fml manager API service +// @version v1 +// @description backend APIs of fml manager service +// @termsOfService http://swagger.io/terms/ +// @contact.name FedLCM team +// @BasePath /api/v1 +// @in header +func main() { + viper.AutomaticEnv() + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) + + log.Logger = log.Output( + zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }, + ).With().Caller().Stack().Logger().Level(zerolog.InfoLevel) + debugLog, _ := strconv.ParseBool(viper.GetString("fmlmanager.debug")) + if debugLog { + log.Logger = log.Logger.Level(zerolog.DebugLevel) + } + + if err := initDB(); err != nil { + panic(err) + } + r := createGinEngine() + initRouter(r) + + tlsEnabled := viper.GetBool("fmlmanager.tls.enabled") + if tlsEnabled { + fmlManagerServerCert := viper.GetString("fmlmanager.tls.server.cert") + fmlManagerServerKey := viper.GetString("fmlmanager.tls.server.key") + caCertPath := viper.GetString("fmlmanager.tls.ca.cert") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + log.Error().Err(err).Msg("read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + tlsConfig := &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: pool, + } + tlsPort := viper.GetString("fmlmanager.tls.port") + if tlsPort == "" { + tlsPort = "8443" + } + server := http.Server{ + Addr: ":" + tlsPort, + Handler: r, + TLSConfig: tlsConfig, + } + log.Info().Msgf("Listening and serving HTTPs on %s", server.Addr) + if err := server.ListenAndServeTLS(fmlManagerServerCert, fmlManagerServerKey); err != nil { + log.Error().Err(err).Msg("gin run error with TLS, ") + return + } + } else { + err := r.Run() + if err != nil { + log.Error().Err(err).Msg("gin run error, ") + return + } + } +} + +func createGinEngine() *gin.Engine { + r := gin.New() + + r.Use(gin.Recovery()) + r.Use(logger.SetLogger( + logger.WithUTC(true), + logger.WithLogger(logging.GetGinLogger))) + + return r +} + +func initDB() error { + var err error + + for i := 0; i < 3; i++ { + err = gorm.InitDB() + if err == nil { + return nil + } + time.Sleep(5 * time.Second) + } + + return fmt.Errorf("initialization failed: %s", err) +} + +func initRouter(r *gin.Engine) { + + v1 := r.Group("/api/" + constants.APIVersion) + { + v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + v1.GET("/status", func(c *gin.Context) { + c.JSON(200, gin.H{ + "msg": "The service is running", + "api_version": constants.APIVersion, + "source_commit": constants.Commit, + "source_branch": constants.Branch, + "build_time": constants.BuildTime, + }) + }) + + // site management + siteRepo := &gorm.SiteRepo{} + siteRepo.InitTable() + api.NewSiteController(siteRepo).Route(v1) + + // project management + projectRepo := &gorm.ProjectRepo{} + projectRepo.InitTable() + projectParticipantRepo := &gorm.ProjectParticipantRepo{} + projectParticipantRepo.InitTable() + projectInvitationRepo := &gorm.ProjectInvitationRepo{} + projectInvitationRepo.InitTable() + projectDataRepo := &gorm.ProjectDataRepo{} + projectDataRepo.InitTable() + api.NewProjectController(projectRepo, siteRepo, projectParticipantRepo, projectInvitationRepo, projectDataRepo).Route(v1) + + // job management repo + jobRepo := &gorm.JobRepo{} + jobRepo.InitTable() + jobParticipantRepo := &gorm.JobParticipantRepo{} + jobParticipantRepo.InitTable() + api.NewJobController(jobRepo, jobParticipantRepo, projectRepo, siteRepo, projectDataRepo).Route(v1) + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..5dae885c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,43 @@ +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..77e4bc74 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,23 @@ +# Lifecycle Manager Frontend UI + +This frontend UI of Lifecycle Manager is based on Clarity and Angular. + +# Start a Dev Environment: +1. Run `npm install` under "frontend" directory. +2. create "proxy.config.json" file under "frontend" directory, and replace the URL of target with your available backend server. +``` + { + "/api/v1": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "headers": { + "Connection": "keep-alive" + } + } + } +``` +3. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +4. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 00000000..ee253b08 --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,127 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "lifecycle-manager": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/lifecycle-manager", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": true + }, + "styles": [ + "node_modules/@clr/ui/clr-ui.min.css", + "src/styles.scss", + "node_modules/codemirror/lib/codemirror.css", + "node_modules/codemirror/addon/hint/show-hint.css", + "node_modules/codemirror/theme/ambiance.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "lifecycle-manager:build", + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "browserTarget": "lifecycle-manager:build:production" + }, + "development": { + "browserTarget": "lifecycle-manager:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "lifecycle-manager:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + }, + "defaultProject": "lifecycle-manager" +} \ No newline at end of file diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js new file mode 100644 index 00000000..0d4dc637 --- /dev/null +++ b/frontend/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/lifecycle-manager'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..8d62461e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,12144 @@ +{ + "name": "lifecycle-manager", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1201.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1201.4.tgz", + "integrity": "sha512-hGO5NrZxV8Z7sILwokt7H+1sMf+5tJS9PJszvYlIBSzG0LBkOwwLQDb4MD42ATXFru57SXNqMZDVKoi1kTgxAw==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.1.4", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/build-angular": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-12.1.4.tgz", + "integrity": "sha512-9kMdnaU2dr8o7gJpuBsEHLUpa6huF8uZQEd1+jhKfByEY/xTQo8qztvmbhFhrSfDvdYRygNHItpt3pYEoCEOig==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1201.4", + "@angular-devkit/build-optimizer": "0.1201.4", + "@angular-devkit/build-webpack": "0.1201.4", + "@angular-devkit/core": "12.1.4", + "@babel/core": "7.14.6", + "@babel/generator": "7.14.5", + "@babel/helper-annotate-as-pure": "7.14.5", + "@babel/plugin-proposal-async-generator-functions": "7.14.7", + "@babel/plugin-transform-async-to-generator": "7.14.5", + "@babel/plugin-transform-runtime": "7.14.5", + "@babel/preset-env": "7.14.7", + "@babel/runtime": "7.14.6", + "@babel/template": "7.14.5", + "@discoveryjs/json-ext": "0.5.3", + "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@ngtools/webpack": "12.1.4", + "ansi-colors": "4.1.1", + "babel-loader": "8.2.2", + "browserslist": "^4.9.1", + "cacache": "15.2.0", + "caniuse-lite": "^1.0.30001032", + "circular-dependency-plugin": "5.2.2", + "copy-webpack-plugin": "9.0.0", + "core-js": "3.15.1", + "critters": "0.0.10", + "css-loader": "5.2.6", + "css-minimizer-webpack-plugin": "3.0.1", + "find-cache-dir": "3.3.1", + "glob": "7.1.7", + "https-proxy-agent": "5.0.0", + "inquirer": "8.1.1", + "jest-worker": "27.0.2", + "karma-source-map-support": "1.4.0", + "less": "4.1.1", + "less-loader": "10.0.0", + "license-webpack-plugin": "2.3.20", + "loader-utils": "2.0.0", + "mini-css-extract-plugin": "1.6.2", + "minimatch": "3.0.4", + "open": "8.2.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "6.0.1", + "postcss": "8.3.5", + "postcss-import": "14.0.2", + "postcss-loader": "6.1.0", + "postcss-preset-env": "6.7.0", + "raw-loader": "4.0.2", + "regenerator-runtime": "0.13.7", + "resolve-url-loader": "4.0.0", + "rxjs": "6.6.7", + "sass": "1.35.1", + "sass-loader": "12.1.0", + "semver": "7.3.5", + "source-map": "0.7.3", + "source-map-loader": "3.0.0", + "source-map-support": "0.5.19", + "style-loader": "2.0.0", + "stylus": "0.54.8", + "stylus-loader": "6.1.0", + "terser": "5.7.0", + "terser-webpack-plugin": "5.1.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.3.0", + "webpack": "5.44.0", + "webpack-dev-middleware": "5.0.0", + "webpack-dev-server": "3.11.2", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "1.5.2" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + } + } + }, + "@angular-devkit/build-optimizer": { + "version": "0.1201.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1201.4.tgz", + "integrity": "sha512-Hq+mDUe4xIyq4939JZaUkptsM89WnZOk8Qel6mS0T/bxMX/qs+nuGD5o+xDKkuayogbiTrLmyZBib0/90eSXEA==", + "dev": true, + "requires": { + "source-map": "0.7.3", + "tslib": "2.3.0", + "typescript": "4.3.4" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + }, + "typescript": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz", + "integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==", + "dev": true + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1201.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1201.4.tgz", + "integrity": "sha512-eMmbyHyWJZMQ1tfwVdja/iAk/eXJFYrF8b27gDV9gGI7MGB3KJ93AhkbPbcvlw4Hhx4+6M11GfeXzbwH0q9pnQ==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1201.4", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.1.4.tgz", + "integrity": "sha512-KOzGD8JbP/7EeUwPiU5x+fo3ZEQ5R4IVW5WoH92PaO3mdpqXC7UL2MWLct8PUe9il9nqJMvrBMldSSvP9PCT2w==", + "dev": true, + "requires": { + "ajv": "8.6.0", + "ajv-formats": "2.1.0", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + } + }, + "@angular-devkit/schematics": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.1.4.tgz", + "integrity": "sha512-yD3y3pK/K5piOgvALFoCCiPp4H8emNa3yZL+vlpEpewVLpF1MM55LeTxc0PI5s0uqtOGVnvcbA5wYgMm3YsUEA==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.1.4", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@angular/animations": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.1.5.tgz", + "integrity": "sha512-t17n8RuzkY7lYawmI01WuNWf70ttvEPknmS3sh7q2IAsBdRiLQCBmh6Hw5q0SBaQNoRWNIcUWKBwl4EmyXOtrA==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/cli": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-12.1.4.tgz", + "integrity": "sha512-LpyhyqWe3bFcuH3MrXeYoIPI1htjwG1b5ehETfq4qsMvNmuFON6QI+F7EWEpX7lItVQc2bES+ogasTZsZue/uw==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1201.4", + "@angular-devkit/core": "12.1.4", + "@angular-devkit/schematics": "12.1.4", + "@schematics/angular": "12.1.4", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.3.1", + "ini": "2.0.0", + "inquirer": "8.1.1", + "jsonc-parser": "3.0.0", + "npm-package-arg": "8.1.5", + "npm-pick-manifest": "6.1.1", + "open": "8.2.1", + "ora": "5.4.1", + "pacote": "11.3.4", + "resolve": "1.20.0", + "semver": "7.3.5", + "symbol-observable": "4.0.0", + "uuid": "8.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "@angular/common": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.1.5.tgz", + "integrity": "sha512-xs33eMhV79S85nbH5FVmujKY3OoczloW9oOGRe5tCzxsDDv85hadrB8Mbm3qI5WQ99DNk8M2+P0MW6uiyCAWkQ==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/compiler": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.1.5.tgz", + "integrity": "sha512-MBrNCuTFmBiTy+sqmBYRqfBHPBJeONvRWOxHepn/CMtjHJiTZ+PHG61Chu2ySxLBuOZxAGaP4GMcqIc15G742g==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/compiler-cli": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-12.1.5.tgz", + "integrity": "sha512-5YqZ5heKS3G5t3Fg9L26UedNnum8F9yjNDgUq0qvYR2b6nEDOLYgUxuUSRtg6IGDglSgbxpkgkGNd/SHF9Iu8A==", + "dev": true, + "requires": { + "@babel/core": "^7.8.6", + "@babel/types": "^7.8.6", + "canonical-path": "1.0.0", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "source-map": "^0.6.1", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.2.0", + "yargs": "^17.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "@angular/core": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.1.5.tgz", + "integrity": "sha512-okXnhAPxfblAWSe5M/IqrSATi+ul83a9o9PWOTFR/XCVcbx+neE2IwIr1dIDKBKj91b1CFalLS+uOSkN05PNkA==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/forms": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.1.5.tgz", + "integrity": "sha512-ABfBRCZYVbR5MUkMNrYU/ovCjXtCedk0h4phzCimGl6hBcpbumT9z/3m0TsY9sQnLq0BaCfKNklnHwCuO9mSSg==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/platform-browser": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.1.5.tgz", + "integrity": "sha512-UNlmvCY8cS1/WojCJtgf/EMMN8qdPV1IQa6vZ6/ChQiS38YlH+aQEOCcxtSSfMFmWjaz7j67u55vSlkvA4rApQ==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.1.5.tgz", + "integrity": "sha512-4Anc2pRGRnBHzSeWMYOaEs6hUYw09N/WsOg1lHevZ8abwXyo80xOKiZa+kfgFkst0XaY8DzuDQFnj7opcEVdCQ==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/router": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.1.5.tgz", + "integrity": "sha512-ZZFLcaenVFKCyaxD3mU4cuV4RNFwCEN6JLWmkOFkwIt01RVJcvPaPxiv4pfQf5hnk2HeSciMA0J+UIzSu5YKMA==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "dev": true + }, + "@babel/core": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz", + "integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helpers": "^7.14.6", + "@babel/parser": "^7.14.6", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", + "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.15.4.tgz", + "integrity": "sha512-P8o7JP2Mzi0SdC6eWr1zF+AEYvrsZa7GSY1lTayjF5XJhVH0kjLYUZPvTMflP7tBgZoe9gIhTa60QwFpqh/E0Q==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz", + "integrity": "sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", + "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", + "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.15.4.tgz", + "integrity": "sha512-J14f/vq8+hdC2KoWLIQSsGrC9EFBKE4NFts8pfMpymfApds+fPqR30AOUWc4tyr56h9l/GA1Sxv2q3dLZWbQ/g==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-function-name": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "dependencies": { + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/helper-get-function-arity": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6" + }, + "dependencies": { + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz", + "integrity": "sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-wrap-function": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/helper-replace-supers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz", + "integrity": "sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz", + "integrity": "sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "dependencies": { + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/helpers": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "dev": true, + "requires": { + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" + }, + "dependencies": { + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "dev": true + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz", + "integrity": "sha512-eBnpsl9tlhPhpI10kU06JHnrYXwg3+V6CaP2idsCXNef0aeslpqyITXQ74Vfk5uHgY7IG7XP0yIH8b42KSzHog==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.15.4", + "@babel/plugin-proposal-optional-chaining": "^7.14.5" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", + "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz", + "integrity": "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.15.4.tgz", + "integrity": "sha512-M682XWrrLNk3chXCjoPUQWOyYsB93B9z3mRyjtqqYJWDf2mfCdIYgDrA11cgNVhAQieaq6F2fn2f3wI0U4aTjA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", + "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", + "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", + "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz", + "integrity": "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz", + "integrity": "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.15.6.tgz", + "integrity": "sha512-qtOHo7A1Vt+O23qEAX+GdBpqaIuD3i9VRrWgCJeq7WO6H2d14EK3q11urj5Te2MAeK97nMiIdRpwd/ST4JFbNg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.15.4" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", + "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz", + "integrity": "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.15.4.tgz", + "integrity": "sha512-X0UTixkLf0PCCffxgu5/1RQyGGbgZuKoI+vXP4iSbJSYwPb7hu06omsFGBvQ9lJEvwgrxHdS8B5nbfcd8GyUNA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-create-class-features-plugin": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", + "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", + "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", + "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", + "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", + "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz", + "integrity": "sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", + "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" + } + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", + "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", + "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", + "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", + "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz", + "integrity": "sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", + "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", + "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", + "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", + "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz", + "integrity": "sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.15.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz", + "integrity": "sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.4", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.9", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", + "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz", + "integrity": "sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", + "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", + "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", + "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.5.tgz", + "integrity": "sha512-fPMBhh1AV8ZyneiCIA+wYYUH1arzlXR1UMcApjvchDhfKxhy2r2lReJv8uHEyihi4IFIGlr1Pdx7S5fkESDQsg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", + "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.15.8.tgz", + "integrity": "sha512-/daZ8s2tNaRekl9YJa9X4bzjpeRZLt122cpgFnQPLGUe61PH8zMEBmYqKkW5xF5JUEh5buEGXJoQpqBmIbpmEQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.15.4" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", + "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", + "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", + "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz", + "integrity": "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", + "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/preset-env": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.7.tgz", + "integrity": "sha512-itOGqCKLsSUl0Y+1nSfhbuuOlTs0MJk2Iv7iSH+XT/mR8U1zRLO7NjWlYXB47yhK4J/7j+HYty/EhFZDYKa/VA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.7", + "@babel/plugin-proposal-class-properties": "^7.14.5", + "@babel/plugin-proposal-class-static-block": "^7.14.5", + "@babel/plugin-proposal-dynamic-import": "^7.14.5", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", + "@babel/plugin-proposal-json-strings": "^7.14.5", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", + "@babel/plugin-proposal-numeric-separator": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5", + "@babel/plugin-proposal-private-methods": "^7.14.5", + "@babel/plugin-proposal-private-property-in-object": "^7.14.5", + "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.14.5", + "@babel/plugin-transform-async-to-generator": "^7.14.5", + "@babel/plugin-transform-block-scoped-functions": "^7.14.5", + "@babel/plugin-transform-block-scoping": "^7.14.5", + "@babel/plugin-transform-classes": "^7.14.5", + "@babel/plugin-transform-computed-properties": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", + "@babel/plugin-transform-dotall-regex": "^7.14.5", + "@babel/plugin-transform-duplicate-keys": "^7.14.5", + "@babel/plugin-transform-exponentiation-operator": "^7.14.5", + "@babel/plugin-transform-for-of": "^7.14.5", + "@babel/plugin-transform-function-name": "^7.14.5", + "@babel/plugin-transform-literals": "^7.14.5", + "@babel/plugin-transform-member-expression-literals": "^7.14.5", + "@babel/plugin-transform-modules-amd": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-modules-systemjs": "^7.14.5", + "@babel/plugin-transform-modules-umd": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", + "@babel/plugin-transform-new-target": "^7.14.5", + "@babel/plugin-transform-object-super": "^7.14.5", + "@babel/plugin-transform-parameters": "^7.14.5", + "@babel/plugin-transform-property-literals": "^7.14.5", + "@babel/plugin-transform-regenerator": "^7.14.5", + "@babel/plugin-transform-reserved-words": "^7.14.5", + "@babel/plugin-transform-shorthand-properties": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", + "@babel/plugin-transform-sticky-regex": "^7.14.5", + "@babel/plugin-transform-template-literals": "^7.14.5", + "@babel/plugin-transform-typeof-symbol": "^7.14.5", + "@babel/plugin-transform-unicode-escapes": "^7.14.5", + "@babel/plugin-transform-unicode-regex": "^7.14.5", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.14.5", + "babel-plugin-polyfill-corejs2": "^0.2.2", + "babel-plugin-polyfill-corejs3": "^0.2.2", + "babel-plugin-polyfill-regenerator": "^0.2.2", + "core-js-compat": "^3.15.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "dev": true, + "requires": { + "@babel/types": "^7.15.6", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "@cds/city": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cds/city/-/city-1.1.0.tgz", + "integrity": "sha512-S9K+Q39BGOghyLHmR0Wdcmu1i1noSUk8HcvMj+3IaohZw02WFd99aPTQDHJeseXrXZP3CNovaSlePI0R11NcFg==", + "optional": true + }, + "@cds/core": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@cds/core/-/core-5.6.0.tgz", + "integrity": "sha512-gx5lRNeAT25WrebdYepOKJMjlh506rDr2gvL4kb88cRzIvR5SxrJ+dK447xUWuaZcXtHn4L1XfDT9E4Szj3TaA==", + "requires": { + "@cds/city": "^1.1.0", + "@types/resize-observer-browser": "^0.1.5", + "lit": "2.0.0-rc.2", + "modern-normalize": "1.1.0", + "ramda": "^0.27.1", + "tslib": "^2.2.0" + } + }, + "@clr/angular": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@clr/angular/-/angular-12.0.6.tgz", + "integrity": "sha512-G7ifvoch7dQKF8g5S1NuxoTsEfcFCmoMnb9GvtjXPZgZ5HnHxFnfEvcbbhfM+t8Mj05QYMKeMatjzmqwVcyfHQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@clr/icons": { + "version": "12.0.7", + "resolved": "https://registry.npmjs.org/@clr/icons/-/icons-12.0.7.tgz", + "integrity": "sha512-niUa+F3QeQhUrTNrCuQQI2fsW85mfrdolqB6cUMPVReEI7qm9Yf8G3UBXOfdT/+KOs4L1ti9tDvbs8lUYpq4zg==" + }, + "@clr/ui": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@clr/ui/-/ui-12.0.6.tgz", + "integrity": "sha512-KFxBYCCAwd7LTaJQL86T+4fREIMxf8Kd+ZBCwBLTY8cO5ymeDLIzN1UemYILY0yLPQ/SMk7mcnv9LrKh5ykghA==" + }, + "@csstools/convert-colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", + "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", + "dev": true + }, + "@discoveryjs/json-ext": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", + "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jsdevtools/coverage-istanbul-loader": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", + "dev": true, + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.7.0" + } + }, + "@lit/reactive-element": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.2.tgz", + "integrity": "sha512-oz3d3MKjQ2tXynQgyaQaMpGTDNyNDeBdo6dXf1AbjTwhA1IRINHmA7kSaVYv9ttKweNkEoNqp9DqteDdgWzPEg==" + }, + "@ngtools/webpack": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.1.4.tgz", + "integrity": "sha512-hXc8dTnRfDB7o1Pd7a07aw0rjGmVLXU28+cTHQJliosgD3obcjfZ4QPA0k97vlQMtqVJawuShRfyiUKrpsJf8Q==", + "dev": true, + "requires": { + "enhanced-resolve": "5.8.2" + } + }, + "@ngx-translate/core": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-13.0.0.tgz", + "integrity": "sha512-+tzEp8wlqEnw0Gc7jtVRAJ6RteUjXw6JJR4O65KlnxOmJrCGPI0xjV/lKRnQeU0w4i96PQs/jtpL921Wrb7PWg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngx-translate/http-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-6.0.0.tgz", + "integrity": "sha512-LCekn6qCbeXWlhESCxU1rAbZz33WzDG0lI7Ig0pYC1o5YxJWrkU9y3Y4tNi+jakQ7R6YhTR2D3ox6APxDtA0wA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/git": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", + "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", + "dev": true, + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz", + "integrity": "sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA==", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz", + "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==", + "dev": true, + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/run-script": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.6.tgz", + "integrity": "sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g==", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^7.1.0", + "read-package-json-fast": "^2.0.1" + } + }, + "@schematics/angular": { + "version": "12.1.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.1.4.tgz", + "integrity": "sha512-xGqgGI6GWk4EFdKis8FmSESxoLgjnLQbaRE1t1KZCkSKJzqkOj0R9wiDrtZfcrbPxIkLL+3fAk2ThwwPznT6yw==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.1.4", + "@angular-devkit/schematics": "12.1.4", + "jsonc-parser": "3.0.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true + }, + "@types/codemirror": { + "version": "5.60.5", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz", + "integrity": "sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==", + "dev": true, + "requires": { + "@types/tern": "*" + } + }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", + "dev": true + }, + "@types/eslint": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.2.tgz", + "integrity": "sha512-KubbADPkfoU75KgKeKLsFHXnU4ipH7wYg0TRT33NK3N3yiu7jlFAAoygIWBV+KbuHx/G+AvuGX6DllnK35gfJA==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", + "dev": true + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz", + "integrity": "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/node": { + "version": "12.20.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.34.tgz", + "integrity": "sha512-+G6kIkmDOyWs7Co8M48lgyauuOlgZeRib64/DFBwYlY6ngwT7wgcF7ga1DsmZImUDfm2rE1jMnUhIEC/gdJ5rw==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/resize-observer-browser": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz", + "integrity": "sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==", + "optional": true + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/tern": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", + "integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "@types/webpack-sources": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.9.tgz", + "integrity": "sha512-bvzMnzqoK16PQIC8AYHNdW45eREJQMd6WG/msQWX5V2+vZmODCOPb4TJcbgRljTZZTwTM4wUMcsI8FftNA7new==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webcomponents/webcomponentsjs": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.6.0.tgz", + "integrity": "sha512-Moog+Smx3ORTbWwuPqoclr+uvfLnciVd6wdCaVscHPrxbmQ/IJKm3wbB7hpzJtXWjAq2l/6QMlO85aZiOdtv5Q==" + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "dev": true + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz", + "integrity": "sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", + "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-formats": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", + "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", + "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "picocolors": "^0.2.1", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-loader": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", + "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.2", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.5.tgz", + "integrity": "sha512-ninF5MQNwAX9Z7c9ED+H2pGt1mXdP4TqzlHKyPIYmJIYz0N+++uwdM7RnJukklhzJ54Q84vA4ZJkgs7lu5vqcw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.16.2" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", + "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz", + "integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001271", + "electron-to-chromium": "^1.3.878", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.2.0.tgz", + "integrity": "sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001271", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz", + "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==", + "dev": true + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "circular-dependency-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", + "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codemirror": { + "version": "5.65.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.1.tgz", + "integrity": "sha512-s6aac+DD+4O2u1aBmdxhB7yz2XU7tG3snOyQ05Kxifahz7hoxnfxIRHxiCSEv3TUC38dIVH8G+lZH9UWSfGQxA==" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colord": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz", + "integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==", + "dev": true + }, + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-anything": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz", + "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==", + "dev": true, + "requires": { + "is-what": "^3.12.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.0.0.tgz", + "integrity": "sha512-k8UB2jLIb1Jip2nZbCz83T/XfhfjX6mB1yLJNYKrpYi7FQimfOoFv/0//iT6HV1K8FwUB5yUbCcnpLebJXJTug==", + "dev": true, + "requires": { + "fast-glob": "^3.2.5", + "glob-parent": "^6.0.0", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "core-js": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.1.tgz", + "integrity": "sha512-h8VbZYnc9pDzueiS2610IULDkpFFPunHwIpl8yRwFahAEEdSpHlTy3h3z3rKq5h11CaUdBEeRViu9AYvbxiMeg==", + "dev": true + }, + "core-js-compat": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.18.3.tgz", + "integrity": "sha512-4zP6/y0a2RTHN5bRGT7PTq9lVt3WzvffTNjqnTKsXhkAYNDTkdCLOIfAdOLcQ/7TDdyRj3c+NeHe1NmF1eDScw==", + "dev": true, + "requires": { + "browserslist": "^4.17.3", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "critters": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.10.tgz", + "integrity": "sha512-p5VKhP1803+f+0Jq5P03w1SbiHtpAKm+1EpJHkiPxQPq0Vu9QLZHviJ02GRrWi0dlcJqrmzMWInbwp4d22RsGw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "css": "^3.0.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "pretty-bytes": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-blank-pseudo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", + "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-color-names": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", + "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", + "dev": true + }, + "css-declaration-sorter": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz", + "integrity": "sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA==", + "dev": true, + "requires": { + "timsort": "^0.3.0" + } + }, + "css-has-pseudo": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", + "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0-rc.4" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-loader": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.6.tgz", + "integrity": "sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "css-minimizer-webpack-plugin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.1.tgz", + "integrity": "sha512-RGFIv6iZWUPO2T1vE5+5pNCSs2H2xtHYRdfZPiiNH8Of6QOn9BeFnZSoHiQMkmsxOO/JkPe4BpKfs7slFIWcTA==", + "dev": true, + "requires": { + "cssnano": "^5.0.0", + "jest-worker": "^27.0.2", + "p-limit": "^3.0.2", + "postcss": "^8.2.9", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-parse": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", + "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=", + "dev": true, + "requires": { + "css": "^2.0.0" + }, + "dependencies": { + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + } + } + }, + "css-prefers-color-scheme": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", + "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-what": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "dev": true + }, + "cssdb": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", + "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.8.tgz", + "integrity": "sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==", + "dev": true, + "requires": { + "cssnano-preset-default": "^5.1.4", + "is-resolvable": "^1.1.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "cssnano-preset-default": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz", + "integrity": "sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==", + "dev": true, + "requires": { + "css-declaration-sorter": "^6.0.3", + "cssnano-utils": "^2.0.1", + "postcss-calc": "^8.0.0", + "postcss-colormin": "^5.2.0", + "postcss-convert-values": "^5.0.1", + "postcss-discard-comments": "^5.0.1", + "postcss-discard-duplicates": "^5.0.1", + "postcss-discard-empty": "^5.0.1", + "postcss-discard-overridden": "^5.0.1", + "postcss-merge-longhand": "^5.0.2", + "postcss-merge-rules": "^5.0.2", + "postcss-minify-font-values": "^5.0.1", + "postcss-minify-gradients": "^5.0.2", + "postcss-minify-params": "^5.0.1", + "postcss-minify-selectors": "^5.1.0", + "postcss-normalize-charset": "^5.0.1", + "postcss-normalize-display-values": "^5.0.1", + "postcss-normalize-positions": "^5.0.1", + "postcss-normalize-repeat-style": "^5.0.1", + "postcss-normalize-string": "^5.0.1", + "postcss-normalize-timing-functions": "^5.0.1", + "postcss-normalize-unicode": "^5.0.1", + "postcss-normalize-url": "^5.0.2", + "postcss-normalize-whitespace": "^5.0.1", + "postcss-ordered-values": "^5.0.2", + "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-transforms": "^5.0.1", + "postcss-svgo": "^5.0.2", + "postcss-unique-selectors": "^5.0.1" + } + }, + "cssnano-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "dev": true + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "requires": { + "css-tree": "^1.1.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==", + "dev": true + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domhandler": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.878", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.878.tgz", + "integrity": "sha512-O6yxWCN9ph2AdspAIszBnd9v8s11hQx8ub9w4UGApzmNRnoKhbulOWqbO8THEQec/aEHtvy+donHZMlh6l1rbA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.0.0.tgz", + "integrity": "sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==", + "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "dev": true + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.1.tgz", + "integrity": "sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==", + "dev": true, + "requires": { + "base64-arraybuffer": "~1.0.1" + } + }, + "enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.7.1.tgz", + "integrity": "sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "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" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "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" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + }, + "inquirer": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.1.1.tgz", + "integrity": "sha512-hUDjc3vBkh/uk1gPfMAD/7Z188Q8cvTGl0nxwaCdwSbzFh6ZKkZh+s2ozVxbE5G9ZNRyeY0+lgbAIOUFsFf98w==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.3.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.6", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.5.tgz", + "integrity": "sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jasmine-core": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz", + "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", + "dev": true + }, + "jest-worker": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.2.tgz", + "integrity": "sha512-EoBdilOTTyOgmHXtw/cPc+ZrCA0KJMrkXzkrPGNwLmnvvlN1nj7MPrxpT7m+otSv2e1TLaVffzDnE/LB14zJMg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "karma": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.5.tgz", + "integrity": "sha512-uAwOPXtfEdIHIZdK7QcnKEv0BK/bg+AZikZ7TK1ZLk3apW/wNH74rbdamJby1CVVYw3I9PXxqeEo78OaiHeErA==", + "dev": true, + "requires": { + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "colors": "^1.4.0", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.3.0", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.28", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, + "karma-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.0.3.tgz", + "integrity": "sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.1", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "minimatch": "^3.0.4" + } + }, + "karma-jasmine": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", + "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", + "dev": true, + "requires": { + "jasmine-core": "^3.6.0" + } + }, + "karma-jasmine-html-reporter": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", + "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", + "dev": true + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "dev": true + }, + "less": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.1.tgz", + "integrity": "sha512-w09o8tZFPThBscl5d0Ggp3RcrKIouBoQscnOMgFH3n5V3kN/CXGHNfCkRPtxJk6nKryDXaV9aHLK55RXuH4sAw==", + "dev": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "less-loader": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-10.0.0.tgz", + "integrity": "sha512-JjioAkw9qyavL0BzMPUOHJa0a20fh+ipq/MNZH4OkU8qERsCMeZIWRE0FDBIx2O+cFguvY01vHh/lmBA9LyWDg==", + "dev": true, + "requires": { + "klona": "^2.0.4" + } + }, + "license-webpack-plugin": { + "version": "2.3.20", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.20.tgz", + "integrity": "sha512-AHVueg9clOKACSHkhmEI+PCC9x8+qsQVuKECZD3ETxETK5h/PCv5/MUzyG1gm8OMcip/s1tcNxqo9Qb7WhjGsg==", + "dev": true, + "requires": { + "@types/webpack-sources": "^0.1.5", + "webpack-sources": "^1.2.0" + } + }, + "lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "lit": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0-rc.2.tgz", + "integrity": "sha512-BOCuoJR04WaTV8UqTKk09cNcQA10Aq2LCcBOiHuF7TzWH5RNDsbCBP5QM9sLBSotGTXbDug/gFO08jq6TbyEtw==", + "requires": { + "@lit/reactive-element": "^1.0.0-rc.2", + "lit-element": "^3.0.0-rc.2", + "lit-html": "^2.0.0-rc.3" + } + }, + "lit-element": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.2.tgz", + "integrity": "sha512-9vTJ47D2DSE4Jwhle7aMzEwO2ZcOPRikqfT3CVG7Qol2c9/I4KZwinZNW5Xv8hNm+G/enSSfIwqQhIXi6ioAUg==", + "requires": { + "@lit/reactive-element": "^1.0.0", + "lit-html": "^2.0.0" + } + }, + "lit-html": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.2.tgz", + "integrity": "sha512-dON7Zg8btb14/fWohQLQBdSgkoiQA4mIUy87evmyJHtxRq7zS6LlC32bT5EPWiof5PUQaDpF45v2OlrxHA5Clg==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log4js": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", + "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", + "dev": true, + "requires": { + "date-format": "^3.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.1", + "rfdc": "^1.1.4", + "streamroller": "^2.2.4" + } + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + } + } + }, + "memfs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", + "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", + "dev": true, + "requires": { + "fs-monkey": "1.0.3" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", + "dev": true + }, + "mime-types": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "dev": true, + "requires": { + "mime-db": "1.50.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", + "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "minipass": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "modern-normalize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "optional": true + }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "dev": true, + "optional": true + }, + "nanocolors": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz", + "integrity": "sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==", + "dev": true + }, + "nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, + "node-gyp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", + "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz", + "integrity": "sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz", + "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==", + "dev": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "npm-registry-fetch": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz", + "integrity": "sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA==", + "dev": true, + "requires": { + "make-fetch-happen": "^9.0.1", + "minipass": "^3.1.3", + "minipass-fetch": "^1.3.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz", + "integrity": "sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + } + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pacote": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.3.4.tgz", + "integrity": "sha512-RfahPCunM9GI7ryJV/zY0bWQiokZyLqaSNHXtbNSoLb7bwTvBbJBEyCJ01KWs4j1Gj7GmX8crYXQ1sNX6P2VKA==", + "dev": true, + "requires": { + "@npmcli/git": "^2.0.1", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^1.8.2", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^2.1.4", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^11.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "dev": true, + "requires": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.5.tgz", + "integrity": "sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + } + }, + "postcss-attribute-case-insensitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", + "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-calc": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz", + "integrity": "sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-color-functional-notation": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", + "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-color-gray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", + "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-color-hex-alpha": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", + "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "dev": true, + "requires": { + "postcss": "^7.0.14", + "postcss-values-parser": "^2.0.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-color-mod-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", + "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-color-rebeccapurple": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", + "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-colormin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz", + "integrity": "sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==", + "dev": true, + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-convert-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz", + "integrity": "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-custom-media": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", + "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-custom-properties": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", + "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", + "dev": true, + "requires": { + "postcss": "^7.0.17", + "postcss-values-parser": "^2.0.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-custom-selectors": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", + "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-dir-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", + "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "dev": true + }, + "postcss-discard-duplicates": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "dev": true + }, + "postcss-discard-empty": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "dev": true + }, + "postcss-discard-overridden": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "dev": true + }, + "postcss-double-position-gradients": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", + "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", + "dev": true, + "requires": { + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-env-function": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", + "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-focus-visible": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", + "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-focus-within": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", + "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-font-variant": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", + "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-gap-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", + "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-image-set-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", + "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-import": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", + "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz", + "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-lab-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", + "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.1.0.tgz", + "integrity": "sha512-yA/cXBfACkthZNA2hQxOnaReVfQ6uLmvbEDQzNafpbK40URZJvP/28dL1DG174Gvz3ptkkHbbwDBCh+gXR94CA==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.4", + "semver": "^7.3.5" + } + }, + "postcss-logical": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", + "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-media-minmax": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", + "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-merge-longhand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz", + "integrity": "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==", + "dev": true, + "requires": { + "css-color-names": "^1.0.1", + "postcss-value-parser": "^4.1.0", + "stylehacks": "^5.0.1" + } + }, + "postcss-merge-rules": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", + "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", + "dev": true, + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^2.0.1", + "postcss-selector-parser": "^6.0.5", + "vendors": "^1.0.3" + } + }, + "postcss-minify-font-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz", + "integrity": "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-minify-gradients": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz", + "integrity": "sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==", + "dev": true, + "requires": { + "colord": "^2.6", + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-minify-params": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", + "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "browserslist": "^4.16.0", + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0", + "uniqs": "^2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz", + "integrity": "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nesting": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", + "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-normalize-charset": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "dev": true + }, + "postcss-normalize-display-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-positions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz", + "integrity": "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz", + "integrity": "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-string": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz", + "integrity": "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz", + "integrity": "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-unicode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz", + "integrity": "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-url": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", + "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", + "dev": true, + "requires": { + "is-absolute-url": "^3.0.3", + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-whitespace": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz", + "integrity": "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-ordered-values": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz", + "integrity": "sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-overflow-shorthand": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", + "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-page-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", + "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-place": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", + "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-preset-env": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", + "integrity": "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==", + "dev": true, + "requires": { + "autoprefixer": "^9.6.1", + "browserslist": "^4.6.4", + "caniuse-lite": "^1.0.30000981", + "css-blank-pseudo": "^0.1.4", + "css-has-pseudo": "^0.10.0", + "css-prefers-color-scheme": "^3.1.1", + "cssdb": "^4.4.0", + "postcss": "^7.0.17", + "postcss-attribute-case-insensitive": "^4.0.1", + "postcss-color-functional-notation": "^2.0.1", + "postcss-color-gray": "^5.0.0", + "postcss-color-hex-alpha": "^5.0.3", + "postcss-color-mod-function": "^3.0.3", + "postcss-color-rebeccapurple": "^4.0.1", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^8.0.11", + "postcss-custom-selectors": "^5.1.2", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-double-position-gradients": "^1.0.0", + "postcss-env-function": "^2.0.2", + "postcss-focus-visible": "^4.0.0", + "postcss-focus-within": "^3.0.0", + "postcss-font-variant": "^4.0.0", + "postcss-gap-properties": "^2.0.0", + "postcss-image-set-function": "^3.0.1", + "postcss-initial": "^3.0.0", + "postcss-lab-function": "^2.0.1", + "postcss-logical": "^3.0.0", + "postcss-media-minmax": "^4.0.0", + "postcss-nesting": "^7.0.0", + "postcss-overflow-shorthand": "^2.0.0", + "postcss-page-break": "^2.0.0", + "postcss-place": "^4.0.1", + "postcss-pseudo-class-any-link": "^6.0.0", + "postcss-replace-overflow-wrap": "^3.0.0", + "postcss-selector-matches": "^4.0.0", + "postcss-selector-not": "^4.0.0" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-pseudo-class-any-link": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", + "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", + "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz", + "integrity": "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-replace-overflow-wrap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", + "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-selector-not": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", + "integrity": "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.2.tgz", + "integrity": "sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0", + "svgo": "^2.3.0" + } + }, + "postcss-unique-selectors": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", + "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "postcss-values-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", + "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + } + } + }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-package-json-fast": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", + "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", + "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "regexpu-core": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", + "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "dev": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^9.0.0", + "regjsgen": "^0.5.2", + "regjsparser": "^0.7.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "regjsparser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", + "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.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" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.35.1.tgz", + "integrity": "sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0" + } + }, + "sass-loader": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.1.0.tgz", + "integrity": "sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg==", + "dev": true, + "requires": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", + "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socket.io": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.3.1.tgz", + "integrity": "sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.0.0", + "socket.io-adapter": "~2.3.2", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz", + "integrity": "sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==", + "dev": true + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dev": true, + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, + "sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + } + }, + "sockjs-client": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz", + "integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "eventsource": "^1.0.7", + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.5.3" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", + "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + }, + "source-map-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.0.tgz", + "integrity": "sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "source-map-js": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "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" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "streamroller": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", + "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", + "dev": true, + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "stylehacks": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", + "integrity": "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "postcss-selector-parser": "^6.0.4" + } + }, + "stylus": { + "version": "0.54.8", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", + "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", + "dev": true, + "requires": { + "css-parse": "~2.0.0", + "debug": "~3.1.0", + "glob": "^7.1.6", + "mkdirp": "~1.0.4", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "semver": "^6.3.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "stylus-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-6.1.0.tgz", + "integrity": "sha512-qKO34QCsOtSJrXxQQmXsPeaVHh6hMumBAFIoJTcsSr2VzrA6o/CW9HCGR8spCjzJhN8oKQHdj/Ytx0wwXyElkw==", + "dev": true, + "requires": { + "fast-glob": "^3.2.5", + "klona": "^2.0.4", + "normalize-path": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.7.0.tgz", + "integrity": "sha512-aDLsGkre4fTDCWvolyW+fs8ZJFABpzLXbtdK1y71CKnHzAnpDxKXPj2mNKj+pyOXUCzFHzuxRJ94XOFygOWV3w==", + "dev": true, + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "nanocolors": "^0.1.12", + "stable": "^0.1.8" + } + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz", + "integrity": "sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A==", + "dev": true, + "requires": { + "jest-worker": "^27.0.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.7.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.30", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.30.tgz", + "integrity": "sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + } + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", + "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webpack": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.44.0.tgz", + "integrity": "sha512-I1S1w4QLoKmH19pX6YhYN0NiSXaWY8Ou00oA+aMcr9IUGeF5azns+IKBkfoAAG9Bu5zOIzZt/mN35OffBya8AQ==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.50", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.8.0", + "es-module-lexer": "^0.7.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.2.0", + "webpack-sources": "^2.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", + "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz", + "integrity": "sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "mem": "^8.1.1", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-dev-server": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", + "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "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-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-subresource-integrity": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz", + "integrity": "sha512-GBWYBoyalbo5YClwWop9qe6Zclp8CIXYGIz12OPclJhIrSplDxs1Ls1JDMH8xBPPrg1T6ISaTW9Y6zOrwEiAzw==", + "dev": true, + "requires": { + "webpack-sources": "^1.3.0" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zone.js": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.4.tgz", + "integrity": "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==", + "requires": { + "tslib": "^2.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..9f36a8d8 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,52 @@ +{ + "name": "lifecycle-manager", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve --open", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "~12.1.1", + "@angular/common": "~12.1.1", + "@angular/compiler": "~12.1.1", + "@angular/core": "~12.1.1", + "@angular/forms": "~12.1.1", + "@angular/platform-browser": "~12.1.1", + "@angular/platform-browser-dynamic": "~12.1.1", + "@angular/router": "~12.1.1", + "@cds/core": "^5.6.0", + "@clr/angular": "^12.0.6", + "@clr/icons": "^12.0.7", + "@clr/ui": "^12.0.6", + "@ngx-translate/core": "^13.0.0", + "@ngx-translate/http-loader": "^6.0.0", + "@webcomponents/webcomponentsjs": "^2.6.0", + "codemirror": "^5.65.1", + "cors": "^2.8.5", + "js-yaml": "^4.1.0", + "jwt-decode": "^3.1.2", + "moment": "^2.29.1", + "rxjs": "~6.6.0", + "tslib": "^2.2.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~12.1.1", + "@angular/cli": "~12.1.1", + "@angular/compiler-cli": "~12.1.1", + "@types/codemirror": "^5.60.5", + "@types/jasmine": "~3.6.0", + "@types/node": "^12.11.1", + "jasmine-core": "~3.7.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", + "typescript": "^4.3.5" + } +} diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 00000000..38f4db60 --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1,12 @@ + { + "/api/v1": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "headers": { + "Connection": "keep-alive" + } + } + } + \ No newline at end of file diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts new file mode 100644 index 00000000..39f4dc4d --- /dev/null +++ b/frontend/src/app/app-routing.module.ts @@ -0,0 +1,222 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes} from '@angular/router'; +import { LoginComponent } from './view/login/login.component'; +import { CertificateMgComponent } from './view/certificate/certificate-mg/certificate-mg.component'; +import { ChartDetailComponent } from './view/chart/chart-detail/chart-detail.component'; +import { ChartMgComponent } from './view/chart/chart-mg/chart-mg.component'; +import { ClusterNewComponent } from './view/federation/cluster-new/cluster-new.component'; +import { ContentComponent } from './view/content.component'; +import { EndpointDetailComponent } from './view/endpoint/endpoint-detail/endpoint-detail.component'; +import { EndpointMgComponent } from './view/endpoint/endpoint-mg/endpoint-mg.component'; +import { EndpointNewComponent } from './view/endpoint/endpoint-new/endpoint-new.component'; +import { ExchangeNewComponent } from './view/federation/exchange-new/exchange-new.component'; +import { FedDetailFateComponent } from './view/federation/fed-detail-fate/fed-detail-fate.component'; +import { FedDetailOpneFLComponent } from './view/openfl/fed-detail-openfl/fed-detail-openfl.component' +import { FedMgComponent } from './view/federation/fed-mg/fed-mg.component'; +import { InfraDetailComponent } from './view/infra/infra-detail/infra-detail.component'; +import { InfraComponent } from './view/infra/infra.component'; +import { CertificateAuthorityDetailComponent } from './view/certificate/certificate-authority-detail/certificate-authority-detail.component' +import { CertificateDetailComponent } from './view/certificate/certificate-detail/certificate-detail.component' +import { ExchangeDetailComponent } from './view/federation/exchange-detail/exchange-detail.component' +import { ClusterDetailComponent } from './view/federation/cluster-detail/cluster-detail.component' +import { TimeOutServiceComponent } from './components/time-out-service/time-out-service.component' +import { DirectorNewComponent } from './view/openfl/director-new/director-new.component' +import { DirectorDetailComponent } from './view/openfl/director-detail/director-detail.component' +import { EnvoyDetailComponent } from './view/openfl/envoy-detail/envoy-detail.component'; +import { AuthService } from './services/common/auth.service'; +import { RouterGuard } from './router-guard'; + +const routes: Routes = [ + { + path: 'login', + component: LoginComponent + }, + { + path: '', + component: ContentComponent, + data: { + preload: true + }, + children: [ + { + path: '', + redirectTo: 'federation', + pathMatch: 'full' + }, + { + path: 'federation', + data: { + preload: true, + }, + component: FedMgComponent + }, + { + path: 'infra', + data: { + preload: true + }, + component: InfraComponent + }, + { + path: 'endpoint', + data: { + preload: true + }, + component: EndpointMgComponent + }, + { + path: 'chart', + data: { + preload: true + }, + component: ChartMgComponent + }, + { + path: 'certificate', + data: { + preload: true + }, + component: CertificateMgComponent + }, + { + path: 'certificate/authority/:id', + data: { + preload: true + }, + component: CertificateAuthorityDetailComponent + }, + { + path: 'certificate/detail/:id', + data: { + preload: true + }, + component: CertificateDetailComponent + }, + + { + path: 'federation/fate/:id', + data: { + preload: true + }, + component: FedDetailFateComponent, + }, + { + path: 'federation/openfl/:id', + data: { + preload: true + }, + component: FedDetailOpneFLComponent, + canActivate: [RouterGuard], + }, + { + path: 'endpoint/new', + data: { + preload: true + }, + component: EndpointNewComponent + }, + { + path: 'federation/fate/:id/exchange/new', + data: { + preload: true + }, + component: ExchangeNewComponent + }, + { + path: 'federation/openfl/:id/director/new', + data: { + preload: true + }, + component: DirectorNewComponent, + canActivate: [RouterGuard], + }, + { + path: 'federation/fate/:id/cluster/new', + data: { + preload: true + }, + component: ClusterNewComponent + }, + { + path: 'federation/fate/:id/exchange/detail/:exchange_uuid', + data: { + preload: true + }, + component: ExchangeDetailComponent + }, + { + path: 'federation/openfl/:id/director/detail/:director_uuid', + data: { + preload: true + }, + component: DirectorDetailComponent, + canActivate: [RouterGuard], + }, + { + path: 'federation/fate/:id/cluster/detail/:cluster_uuid', + data: { + preload: true + }, + + component: ClusterDetailComponent + }, + { + path: 'federation/openfl/:id/envoy/detail/:envoy_uuid', + data: { + preload: true + }, + + component: EnvoyDetailComponent, + canActivate: [RouterGuard], + }, + { + path: 'infra-detail/:id', + data: { + preload: true + }, + component: InfraDetailComponent + }, + { + path: 'endpoint-detail/:id', + data: { + preload: true + }, + component: EndpointDetailComponent + }, + { + path: 'chart-detail/:id', + data: { + preload: true + }, + component: ChartDetailComponent + }, + { + path: 'timeout', + data: { + preload: true + }, + component: TimeOutServiceComponent + } + ] + }, +]; +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + providers:[RouterGuard, AuthService] +}) + +export class AppRoutingModule { +} + diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 00000000..90c6b646 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts new file mode 100644 index 00000000..722cd369 --- /dev/null +++ b/frontend/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'lifecycle-manager'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('lifecycle-manager'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('lifecycle-manager app is running!'); + }); +}); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 00000000..9f754d13 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { addTextIcon, ClarityIcons, userIcon, vmBugIcon, alignBottomIcon, barsIcon, certificateIcon, cogIcon, nodeGroupIcon, organizationIcon, usersIcon, hostGroupIcon, trashIcon, checkCircleIcon, angleIcon, plusCircleIcon, clusterIcon, routerIcon, cloudTrafficIcon, nvmeIcon, refreshIcon, worldIcon, detailsIcon, popOutIcon, timesCircleIcon, searchIcon, recycleIcon, nodesIcon, infoCircleIcon } from '@cds/core/icon'; +import { thinClientIcon } from '@cds/core/icon/shapes/thin-client'; +import { updateIcon } from '@cds/core/icon/shapes/update'; +ClarityIcons.addIcons(addTextIcon, vmBugIcon, userIcon, alignBottomIcon, cogIcon, certificateIcon, organizationIcon, barsIcon, nodeGroupIcon, usersIcon, hostGroupIcon, trashIcon, checkCircleIcon, angleIcon, plusCircleIcon, clusterIcon, routerIcon, cloudTrafficIcon, nvmeIcon, updateIcon, refreshIcon, worldIcon, detailsIcon, popOutIcon, timesCircleIcon, searchIcon, recycleIcon, nodesIcon, thinClientIcon,infoCircleIcon); + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'lifecycle-manager'; +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts new file mode 100644 index 00000000..7cfa8b8d --- /dev/null +++ b/frontend/src/app/app.module.ts @@ -0,0 +1,115 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { AppRoutingModule} from './app-routing.module'; +import { AppComponent } from './app.component'; +import { ClarityModule } from '@clr/angular'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HeaderComponent } from './components/header/header.component'; +import { ContentComponent } from './view/content.component'; +import { SideNavComponent } from './components/side-nav/side-nav.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CertificateMgComponent } from './view/certificate/certificate-mg/certificate-mg.component'; +import { InfraComponent } from './view/infra/infra.component'; +import { FedMgComponent } from './view/federation/fed-mg/fed-mg.component'; +import { EndpointMgComponent } from './view/endpoint/endpoint-mg/endpoint-mg.component'; +import { ChartMgComponent } from './view/chart/chart-mg/chart-mg.component'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { FedDetailFateComponent } from './view/federation/fed-detail-fate/fed-detail-fate.component'; +import { FedDetailOpneFLComponent } from './view/openfl/fed-detail-openfl/fed-detail-openfl.component'; +import { EndpointNewComponent } from './view/endpoint/endpoint-new/endpoint-new.component'; +import { ExchangeNewComponent } from './view/federation/exchange-new/exchange-new.component'; +import { DirectorNewComponent } from './view/openfl/director-new/director-new.component' +import { DirectorDetailComponent } from './view/openfl/director-detail/director-detail.component' +import { ClusterNewComponent } from './view/federation/cluster-new/cluster-new.component'; +import { InfraDetailComponent } from './view/infra/infra-detail/infra-detail.component'; +import { EndpointDetailComponent } from './view/endpoint/endpoint-detail/endpoint-detail.component'; +import { LoginComponent } from './view/login/login.component'; +import { AuthInterceptor } from 'src/utils/auth-interceptor'; + +import { AlertComponent } from './components/alert/alert.component'; +import { SharedModule } from './pipes/shared/shared.module' +import { TranslateModule, TranslateLoader } from '@ngx-translate/core' +import { createTranslateLoader, AppService } from './app.service'; +import { ChartDetailComponent } from './view/chart/chart-detail/chart-detail.component'; +import { CertificateAuthorityDetailComponent } from './view/certificate/certificate-authority-detail/certificate-authority-detail.component'; +import { CertificateDetailComponent } from './view/certificate/certificate-detail/certificate-detail.component'; +import { ExchangeDetailComponent } from './view/federation/exchange-detail/exchange-detail.component'; +import { ClusterDetailComponent } from './view/federation/cluster-detail/cluster-detail.component' +import { MessageModule } from './components/message/message.module'; +import { EventsListComponent } from './components/events-list/events-list.component'; +import { TimeOutServiceComponent } from './components/time-out-service/time-out-service.component'; +import { FilterComponent } from './components/filter/filter.component'; +import { EnvoyDetailComponent } from './view/openfl/envoy-detail/envoy-detail.component'; +import { CreateOpenflComponent } from './view/openfl/create-openfl-fed/create-openfl-fed.component'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + ContentComponent, + SideNavComponent, + CertificateMgComponent, + InfraComponent, + FedMgComponent, + EndpointMgComponent, + ChartMgComponent, + FedDetailFateComponent, + FedDetailOpneFLComponent, + EndpointNewComponent, + ExchangeNewComponent, + ClusterNewComponent, + InfraDetailComponent, + EndpointDetailComponent, + LoginComponent, + AlertComponent, + ChartDetailComponent, + CertificateAuthorityDetailComponent, + ExchangeDetailComponent, + ClusterDetailComponent, + EventsListComponent, + CertificateDetailComponent, + TimeOutServiceComponent, + DirectorNewComponent, + DirectorDetailComponent, + FilterComponent, + EnvoyDetailComponent, + CreateOpenflComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + ClarityModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + HttpClientModule, + SharedModule, + MessageModule, + TranslateModule.forRoot({// config i8n + defaultLanguage: 'en', + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient] + } + }), + ], + providers: [ + [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, AppService] + ], + bootstrap: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class AppModule { } diff --git a/frontend/src/app/app.service.spec.ts b/frontend/src/app/app.service.spec.ts new file mode 100644 index 00000000..3a3b746f --- /dev/null +++ b/frontend/src/app/app.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AppService } from './app.service'; + +describe('AppService', () => { + let service: AppService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AppService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/app.service.ts b/frontend/src/app/app.service.ts new file mode 100644 index 00000000..a26c9014 --- /dev/null +++ b/frontend/src/app/app.service.ts @@ -0,0 +1,38 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core' +import { HttpClient } from '@angular/common/http'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +@Injectable() +export class AppService { + public lang = 'en' + public langList = ['en', 'zh_CN'] + constructor (public translate: TranslateService) { + const lang = localStorage.getItem('Lifecycle-Manager-Language') + if (lang) this.lang = lang + this.translate.addLangs(this.langList) + this.translate.setDefaultLang(this.lang) + this.translate.use(this.lang) + } + + //changeLanguage is for change language button on the header + changeLanguage (lang: string) { + this.lang = lang + localStorage.setItem('Lifecycle-Manager-Language', lang) + this.translate.setDefaultLang(this.lang) + this.translate.use(this.lang) + } +} +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, '../assets/i18n/', '.json') +} diff --git a/frontend/src/app/components/alert/alert.component.html b/frontend/src/app/components/alert/alert.component.html new file mode 100644 index 00000000..0b273ed2 --- /dev/null +++ b/frontend/src/app/components/alert/alert.component.html @@ -0,0 +1,8 @@ + + + {{message}} + + \ No newline at end of file diff --git a/frontend/src/app/components/alert/alert.component.scss b/frontend/src/app/components/alert/alert.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/alert/alert.component.spec.ts b/frontend/src/app/components/alert/alert.component.spec.ts new file mode 100644 index 00000000..8e3decb3 --- /dev/null +++ b/frontend/src/app/components/alert/alert.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AlertComponent } from './alert.component'; + +describe('AlertComponent', () => { + let component: AlertComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AlertComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/alert/alert.component.ts b/frontend/src/app/components/alert/alert.component.ts new file mode 100644 index 00000000..2307ddd8 --- /dev/null +++ b/frontend/src/app/components/alert/alert.component.ts @@ -0,0 +1,38 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, Input } from '@angular/core'; + +@Component({ + selector: 'app-alert', + templateUrl: './alert.component.html', + styleUrls: ['./alert.component.scss'] +}) +export class AlertComponent implements OnInit { + + constructor() { } + @Input()type: 'success' | 'info' | 'warning' | 'danger' = 'info' + @Input()message: string = '' + @Input() width:string ='100%' + @Input() untreatedMargin: string = '0' + get margin () { + let margin = '' + if (this.untreatedMargin.indexOf(',') !== -1) { + margin = this.untreatedMargin.replace(/,/g, ' ') + } else { + margin = this.untreatedMargin + } + return margin + } + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/components/components-loader.ts b/frontend/src/app/components/components-loader.ts new file mode 100644 index 00000000..eb1c63fb --- /dev/null +++ b/frontend/src/app/components/components-loader.ts @@ -0,0 +1,64 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + ComponentFactoryResolver, + ComponentRef, + Type, + Injector, + Provider, + ElementRef, + ComponentFactory +} from '@angular/core' +export class ComponentLoader { + constructor(private _cfr: ComponentFactoryResolver, + private _injector: Injector) { + } + private _componentFactory: ComponentFactory | any + attch(componentType: Type): ComponentLoader { + this._componentFactory = this._cfr.resolveComponentFactory(componentType) + return this + } + private _parent: Element | any + to(parent: string | ElementRef): ComponentLoader { + if (parent instanceof ElementRef) { + this._parent = parent.nativeElement + } else { + this._parent = document.querySelector(parent) + } + return this + } + private _providers: Provider[] = [] + provider(provider: Provider) { + this._providers.push(provider) + } + create(opts: {}): ComponentRef { + const injector = Injector.create({ + providers: this._providers as any[], + parent: this._injector, + name: '$msg' + }) + const componentRef = this._componentFactory.create(injector) + Object.assign(componentRef.instance, opts) + if (this._parent) { + this._parent.appendChild(componentRef.location.nativeElement) + } + componentRef.changeDetectorRef.markForCheck(); + componentRef.changeDetectorRef.detectChanges(); + return componentRef; + } + remove(ref: ComponentRef | any) { + if (this._parent) { + this._parent.removeChild(ref.location.nativeElement) + } + ref = null; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/events-list/event.service.spec.ts b/frontend/src/app/components/events-list/event.service.spec.ts new file mode 100644 index 00000000..df6b1685 --- /dev/null +++ b/frontend/src/app/components/events-list/event.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EventService } from './event.service'; + +describe('EventService', () => { + let service: EventService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EventService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/events-list/event.service.ts b/frontend/src/app/components/events-list/event.service.ts new file mode 100644 index 00000000..067e5455 --- /dev/null +++ b/frontend/src/app/components/events-list/event.service.ts @@ -0,0 +1,14 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + + +@Injectable({ + providedIn: 'root' +}) +export class EventService { + + constructor(private http: HttpClient) { } + getEventList(entity_uuid: string) { + return this.http.get('/event/' + entity_uuid); + } +} diff --git a/frontend/src/app/components/events-list/events-list.component.html b/frontend/src/app/components/events-list/events-list.component.html new file mode 100644 index 00000000..269b436e --- /dev/null +++ b/frontend/src/app/components/events-list/events-list.component.html @@ -0,0 +1,27 @@ +
+ + + {{errorMessage}} + + + + + +
+ + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'| translate}} + {{'Event.logLevel'| translate}} + {{'CommonlyUse.description'| translate}} + + + + {{constantGather('eventType', event.type).name | translate}} + {{event.created_at | date : "medium"}} + {{event.data.log_level}} + {{event.data.description}} + + + {{eventList.length}} {{'CommonlyUse.item'| translate}} + \ No newline at end of file diff --git a/frontend/src/app/components/events-list/events-list.component.scss b/frontend/src/app/components/events-list/events-list.component.scss new file mode 100644 index 00000000..4cf75673 --- /dev/null +++ b/frontend/src/app/components/events-list/events-list.component.scss @@ -0,0 +1,4 @@ +.refreshbtn { + float: right; + margin-right: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/components/events-list/events-list.component.spec.ts b/frontend/src/app/components/events-list/events-list.component.spec.ts new file mode 100644 index 00000000..d27a392d --- /dev/null +++ b/frontend/src/app/components/events-list/events-list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EventsListComponent } from './events-list.component'; + +describe('EventsListComponent', () => { + let component: EventsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EventsListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EventsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/events-list/events-list.component.ts b/frontend/src/app/components/events-list/events-list.component.ts new file mode 100644 index 00000000..61e8be28 --- /dev/null +++ b/frontend/src/app/components/events-list/events-list.component.ts @@ -0,0 +1,60 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, OnInit } from '@angular/core'; +import { EventService } from './event.service'; +import { EventType, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-events-list', + templateUrl: './events-list.component.html', + styleUrls: ['./events-list.component.scss'] +}) + +export class EventsListComponent implements OnInit { + @Input('entity-uuid') entity_uuid: string = ''; + constructor(private eventService: EventService) { } + + ngOnInit(): void { + this.showEventlist() + } + + eventType = EventType; + constantGather = constantGather + + eventList: any = [] + errorMessage: any; + isShowEndpointDetailFailed: boolean = false; + isPageLoading: boolean = true; + isShowEventlistFailed = false; + //showEventlist is to get the event list by entity's UUID + showEventlist() { + this.isPageLoading = true; + this.isShowEventlistFailed = false; + this.eventService.getEventList(this.entity_uuid) + .subscribe((data: any) => { + if (data.data) this.eventList = data.data; + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isPageLoading = false; + this.isShowEndpointDetailFailed = true; + } + ); + } + + //refresh is for refresh button + refresh() { + this.showEventlist() + } + +} diff --git a/frontend/src/app/components/filter/filter.component.html b/frontend/src/app/components/filter/filter.component.html new file mode 100644 index 00000000..4b2c380f --- /dev/null +++ b/frontend/src/app/components/filter/filter.component.html @@ -0,0 +1,17 @@ +
+
+
+ + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/filter/filter.component.scss b/frontend/src/app/components/filter/filter.component.scss new file mode 100644 index 00000000..145cbafa --- /dev/null +++ b/frontend/src/app/components/filter/filter.component.scss @@ -0,0 +1,23 @@ + +.searchinput { + width: 150px; +} + +.clr-form{ + position: relative; + margin-bottom: 30px; + .searchbtn { + position: absolute; + .search { + position: absolute; + right: 0px; + } + } + &.position { + .clr-row::ng-deep { + .clr-form-control { + margin-top: 0px; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/filter/filter.component.spec.ts b/frontend/src/app/components/filter/filter.component.spec.ts new file mode 100644 index 00000000..f43cc0d6 --- /dev/null +++ b/frontend/src/app/components/filter/filter.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterComponent } from './filter.component'; + +describe('FilterComponent', () => { + let component: FilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/filter/filter.component.ts b/frontend/src/app/components/filter/filter.component.ts new file mode 100644 index 00000000..bc96ea32 --- /dev/null +++ b/frontend/src/app/components/filter/filter.component.ts @@ -0,0 +1,93 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +@Component({ + selector: 'app-filter', + templateUrl: './filter.component.html', + styleUrls: ['./filter.component.scss'] +}) +export class FilterComponent implements OnInit, OnChanges { + constructor() { + + } + ngOnChanges(changes: SimpleChanges): void { + this._datalist = this.dataList.map(el => { + if (typeof (el[this.searchKey]) === 'string') { + el[this.searchKey] = el[this.searchKey].toLowerCase() + if (this.searchKey2 !== '') { + el[this.searchKey2] = el[this.searchKey2].toLowerCase() + } + return el + } else { + return el + } + }) + } + @Input() left = 'auto' + @Input() right = '0' + @Input() top = 'auto' + @Input() bottom = 'auto' + @Input() width = 300 + @Input() dataList: any[] = [] // data + @Input() searchKey: string = '' + @Input() searchKey2: string = '' + @Input() placeholder: string = '' + @Output() filterDataList = new EventEmitter() + fixDataView() { + const data = { + searchValue: this.filterSearchValue, + eligibleList: this.eligibleList + } + this.filterDataList.emit(data) + } + public filterSearchValue: string = '' + public eligibleList: any[] = [] + private _datalist: any[] = [] + private _filterSearchValue = '' + private storageDataList: any[] = [] + ngOnInit(): void { + this.storageDataList = this.dataList + } + + inputHandle() { + this.eligibleList = [] + this._filterSearchValue = this.filterSearchValue.toLowerCase() + if (!this.filterSearchValue.trim()) { + this.fixDataView() + return + } + this._datalist.forEach((el: any, index: number) => { + if (typeof (el[this.searchKey]) === 'string') { + if (el[this.searchKey].indexOf(this._filterSearchValue) !== -1 || (this.searchKey2 !== '' && el[this.searchKey2].indexOf(this._filterSearchValue) !== -1)) { + this.eligibleList.push(this.dataList[index]) + } + } else { + if (Array.isArray(el[this.searchKey])) { + this.isArray(el[this.searchKey], this._filterSearchValue, index) + } else { + + } + } + }); + this.fixDataView() + } + isArray(arr: any[], str: string, index: number) { + arr.forEach(el => { + if (this.eligibleList.find(el => el === this.dataList[index])) return + for (const key in el) { + if (key.toLocaleLowerCase().indexOf(str) !== -1 || el[key].toLocaleLowerCase().indexOf(str) !== -1) { + this.eligibleList.push(this.dataList[index]) + } + } + }) + } +} diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html new file mode 100644 index 00000000..662eee5d --- /dev/null +++ b/frontend/src/app/components/header/header.component.html @@ -0,0 +1,104 @@ +
+ +
+
+ + {{i18.lang | translate}} + + + +
+ +
+ + + {{username}} + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/components/header/header.component.scss b/frontend/src/app/components/header/header.component.scss new file mode 100644 index 00000000..e0cabd4f --- /dev/null +++ b/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,39 @@ +.dropdown{ + right: 25px; + top: 50%; + transform: translateY(-50%); + z-index: 999; +} +.header { +justify-content: space-between; +} +.header-btn-wrap { + position: relative; +} +.dropdown-menu { + right: 6px; + min-width:2px; +} + +.dropdown-toggle.btn.btn-link { + color: #fff; +} +.usericon { + margin: 4px; + margin-bottom: 6px; +} +.angleicon { + margin-right: -4px; + margin-top: -3px; +} +.iconImage { + width: 36px; + height: 36px; + margin-right: 12px; +} +.language-dropdown { + margin-right: 12px; +} +clr-password-container input { + width: 250px +} diff --git a/frontend/src/app/components/header/header.component.spec.ts b/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 00000000..381e8e80 --- /dev/null +++ b/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderComponent } from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HeaderComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts new file mode 100644 index 00000000..3f2067dc --- /dev/null +++ b/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,146 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from 'src/app/services/common/auth.service'; +import { uncompile } from 'src/utils/compile'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ConfirmedValidator } from 'src/utils/validators'; +import { AppService } from 'src/app/app.service' +import { MessageService } from 'src/app/components/message/message.service'; +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'] +}) +export class HeaderComponent implements OnInit { + + form: FormGroup; + constructor(private authService: AuthService, private route: ActivatedRoute, public i18: AppService, + private router: Router, private fb: FormBuilder, private $msg: MessageService) { + //form is for the form in the modal of 'Change Password' + this.form = this.fb.group({ + curPassword: [''], + newPassword: ['', [Validators.required]], + confirmPassword: ['', [Validators.required]] + + }, { + validator: ConfirmedValidator('newPassword', 'confirmPassword') + }) + } + + + username: string = ''; + ngOnInit(): void { + // get stored username + const username = sessionStorage.getItem('username') + if (username) { + this.username = uncompile(username) + } else { + this.authService.getCurrentUser().subscribe( + data => { + if (data.data) { + this.username = data.data + } + }, + err => { + this.router.navigate(['/login']) + } + ) + } + } + + condition: boolean = false; + open: boolean = false; + returnUrl: string = "/login"; + isLoggedIn = true; + isLoginFailed = false; + errorMessage = 'Service Error!'; + isLogoutFailed = false; + isLogOutSubmit = false; + langFlag: boolean = false + //toggleDropdown is for user dropdown button + toggleDropdown() { + this.langFlag = false + this.condition = !this.condition; + } + + //logout is for logout button + logout(): void { + this.isLogOutSubmit = true; + this.authService.logout() + .subscribe( + data => { + this.isLoggedIn = false; + sessionStorage.removeItem('username') + sessionStorage.removeItem('userId') + this.router.navigate([this.returnUrl]) + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isLogoutFailed = true + this.$msg.error(this.errorMessage, 2000) + } + ); + } + //langDropdown is for language dropdown button + langDropdown() { + this.langFlag = !this.langFlag; + this.condition = false + if (this.langFlag) this.condition = false; + } + curPassword: any = ''; + newPassword: any = ''; + confirmPassword: any = ''; + openModal: boolean = false; + //resetModal is to reset the modal when close the modal of 'Change Password' + resetModal() { + this.form.reset(); + this.openModal = false; + this.isChangePwdSubmit = false; + this.isChangePwdFailed = false; + this.isChangePwdSuccessed = false; + } + + isChangePwdSubmit: boolean = false; + isChangePwdFailed: boolean = false; + isChangePwdSuccessed: boolean = false; + //changePassword is for submitting 'Change Password' + changePassword() { + var userId = sessionStorage.getItem('userId'); + this.isChangePwdSubmit = true; + if (!this.form.valid) { + this.isChangePwdFailed = true; + this.errorMessage = "Invaild input." + return; + } + this.authService.changePassword(this.curPassword, this.newPassword, userId) + .subscribe(data => { + this.isChangePwdFailed = false; + this.isChangePwdSuccessed = true; + setTimeout(() => { + this.logout(); + }, 3000) + }, + err => { + this.isChangePwdFailed = true; + //validate if the error is because the current password provided by user is incorrect + if (err.error.message === "crypto/bcrypt: hashedPassword is not the hash of the given password") { + this.errorMessage = "The input of current password is incorrect. " + } else { + this.errorMessage = err.error.message; + } + }); + } + + +} diff --git a/frontend/src/app/components/loaderFactory.ts b/frontend/src/app/components/loaderFactory.ts new file mode 100644 index 00000000..97bdab69 --- /dev/null +++ b/frontend/src/app/components/loaderFactory.ts @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + ComponentFactoryResolver, + Injector, + Injectable +} from '@angular/core'; +import { ComponentLoader } from './components-loader'; + +@Injectable() +export class ComponentLoaderFactory { + constructor(private _injector: Injector, + private _cfr: ComponentFactoryResolver) { + + } + + create(): ComponentLoader { + return new ComponentLoader(this._cfr, this._injector); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/message/message.component.css b/frontend/src/app/components/message/message.component.css new file mode 100644 index 00000000..9ea79e39 --- /dev/null +++ b/frontend/src/app/components/message/message.component.css @@ -0,0 +1,102 @@ +.message { + position: fixed; + z-index: 1999; + width: 100%; + top: 36px; + left: 0; + pointer-events: none; + padding: 8px; + text-align: center; + } + .hide { + display: none; + } + .message-content { + position: relative; + padding: 8px 16px; + padding-left: 40px; + min-width: 300px; + max-width: 800px; + text-align: left; + font-size: 0.65rem; + color: #333; + line-height: 24px; + -ms-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 2px 8px #000000; + -ms-box-shadow: 0 2px 8px #000000; + box-shadow: 0 2px 8px #000000; + box-shadow: 0 2px 8px rgba(0,0,0,.2); + background: #fff; + display: inline-block; + pointer-events: all; + } + .message-content .content-msg { + margin: 0px; + } + .message-content .content-icon { + position: absolute; + left: 16px; + } + .special { + height: 24px; + overflow: hidden; + } + .message-success{ + background: #DFF0D0; + border: 1px solid #306B00; + } + .message-warning { + background: #FDF4C7; + border: 1px solid #AD7600; + } + .message-error{ + background: #FCDDD7; + border: 1px solid #991700; + color: #666; + } + .message-info{ + background: #E3F5FC; + border: 1px solid #00567a; + color: #666; + } + .message-hide{ + display: block; + } +.close { + position: absolute; + top: -10px; + right: -10px; + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #00567a; + text-align: center; + margin: 0px; + line-height: 18px; + color: #00567a; + font-weight: bold; + background: #E3F5FC; + opacity: 1; +} +.close-success { + background: #DFF0D0; + border: 2px solid #306B00; + color: #306B00; +} +.close-warning { + background: #FDF4C7; + border: 2px solid #AD7600; + color: #AD7600; +} +.close-error { + background: #FCDDD7; + border: 2px solid #991700; + color: #991700; +} +.close-info { + background: #E3F5FC; + border: 2px solid #00567a; + color: #00567a; +} \ No newline at end of file diff --git a/frontend/src/app/components/message/message.component.html b/frontend/src/app/components/message/message.component.html new file mode 100644 index 00000000..01ab9f24 --- /dev/null +++ b/frontend/src/app/components/message/message.component.html @@ -0,0 +1,18 @@ +
+
+

X

+ + + + + + +
+ {{messageContent.trim() | translate}} +
{{messageContent.trim() | translate}}
+
用户信息已失效,请重新登录
+
Invaild authentication. Please log in.
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/message/message.component.spec.ts b/frontend/src/app/components/message/message.component.spec.ts new file mode 100644 index 00000000..03cb616f --- /dev/null +++ b/frontend/src/app/components/message/message.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessageComponent } from './message.component'; + +describe('MessageComponent', () => { + let component: MessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MessageComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/message/message.component.ts b/frontend/src/app/components/message/message.component.ts new file mode 100644 index 00000000..dac18545 --- /dev/null +++ b/frontend/src/app/components/message/message.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { ClarityIcons, successStandardIcon, exclamationCircleIcon, exclamationTriangleIcon, infoCircleIcon } from '@cds/core/icon'; +import { AppService } from '../../app.service' +ClarityIcons.addIcons(successStandardIcon); +ClarityIcons.addIcons(exclamationCircleIcon); +ClarityIcons.addIcons(exclamationTriangleIcon); +ClarityIcons.addIcons(infoCircleIcon); +@Component({ + selector: 'app-message', + templateUrl: './message.component.html', + styleUrls: ['./message.component.css'] +}) +export class MessageComponent implements OnInit { + + constructor(public app: AppService) { + } + public classType: string[] = [] + public classCloseType: string[] = [] + public messageContent = '' + ngOnInit(): void { + this.classType = ['message-' + this.messageType] + this.classCloseType = ['close-' + this.messageType] + } + //message type: 'success', 'info', 'warning', 'hide', 'error' + @Input() messageType: 'success' | 'info' | 'warning' | 'hide' | 'error' = 'info' + @ViewChild('msg') msg!: ElementRef; + public distory() { + this.msg.nativeElement.style.display = 'none' + } +} diff --git a/frontend/src/app/components/message/message.module.ts b/frontend/src/app/components/message/message.module.ts new file mode 100644 index 00000000..09bdaea2 --- /dev/null +++ b/frontend/src/app/components/message/message.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MessageComponent } from './message.component' +import { ClarityModule } from '@clr/angular' +import { ComponentLoaderFactory } from '../loaderFactory' +import { MessageService } from './message.service' +import { TranslateModule } from '@ngx-translate/core' + +@NgModule({ + declarations: [MessageComponent], + imports: [ + CommonModule, + ClarityModule, + TranslateModule + ], + providers:[MessageService, ComponentLoaderFactory], + entryComponents: [MessageComponent], + exports: [MessageComponent] +}) +export class MessageModule { } diff --git a/frontend/src/app/components/message/message.service.spec.ts b/frontend/src/app/components/message/message.service.spec.ts new file mode 100644 index 00000000..1db761b5 --- /dev/null +++ b/frontend/src/app/components/message/message.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + let service: MessageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MessageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/message/message.service.ts b/frontend/src/app/components/message/message.service.ts new file mode 100644 index 00000000..c45ef788 --- /dev/null +++ b/frontend/src/app/components/message/message.service.ts @@ -0,0 +1,70 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { ComponentLoaderFactory } from '../loaderFactory' +import { ComponentLoader } from '../components-loader' +import { MessageComponent } from './message.component' +@Injectable({ + providedIn: 'root' +}) +export class MessageService { + + constructor( + private _clf: ComponentLoaderFactory, + private _injector: Injector, + ) { + this._loader = this._clf.create(); + } + public ref: any + private _loader: ComponentLoader + private popUpMessage(t: string, messageContent: string, duration = 10000) { + this._loader.attch(MessageComponent).to('body') + const opts = { + messageType: t, + messageContent: messageContent + } + this.ref = this._loader.create(opts) + this.ref.changeDetectorRef.markForCheck() + this.ref.changeDetectorRef.detectChanges() + setTimeout(() => { + if (this.ref !== '') { + try { + this._loader.remove(this.ref) + } catch (error) { + + } + } + }, duration) + } + public info(messageContent: string, duration?: number) { + this.popUpMessage('info', messageContent, duration); + } + public success(messageContent: string, duration?: number) { + this.popUpMessage('success', messageContent, duration); + } + public error(messageContent: string, duration?: number) { + this.popUpMessage('error', messageContent, duration); + } + public warning(messageContent: string, duration?: number) { + this.popUpMessage('warning', messageContent, duration); + } + public close() { + if (this.ref !== '') { + try { + this._loader.remove(this.ref) + } catch (error) { + + } + this.ref = '' + } + } +} diff --git a/frontend/src/app/components/side-nav/side-nav.component.html b/frontend/src/app/components/side-nav/side-nav.component.html new file mode 100644 index 00000000..a487c86b --- /dev/null +++ b/frontend/src/app/components/side-nav/side-nav.component.html @@ -0,0 +1,22 @@ + + + + {{'Nav.federation'|translate}} + + + + {{'Nav.infraProvider'|translate}} + + + + {{'Nav.endpoint'|translate}} + + + + {{'Nav.chart'|translate}} + + + + {{'Nav.certificate'|translate}} + + \ No newline at end of file diff --git a/frontend/src/app/components/side-nav/side-nav.component.scss b/frontend/src/app/components/side-nav/side-nav.component.scss new file mode 100644 index 00000000..685f7d93 --- /dev/null +++ b/frontend/src/app/components/side-nav/side-nav.component.scss @@ -0,0 +1,4 @@ +clr-vertical-nav { + height: 100%; + width: 220px; +} \ No newline at end of file diff --git a/frontend/src/app/components/side-nav/side-nav.component.spec.ts b/frontend/src/app/components/side-nav/side-nav.component.spec.ts new file mode 100644 index 00000000..188930a9 --- /dev/null +++ b/frontend/src/app/components/side-nav/side-nav.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SideNavComponent } from './side-nav.component'; + +describe('SideNavComponent', () => { + let component: SideNavComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SideNavComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SideNavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/side-nav/side-nav.component.ts b/frontend/src/app/components/side-nav/side-nav.component.ts new file mode 100644 index 00000000..be443e0e --- /dev/null +++ b/frontend/src/app/components/side-nav/side-nav.component.ts @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-side-nav', + templateUrl: './side-nav.component.html', + styleUrls: ['./side-nav.component.scss'] +}) +export class SideNavComponent implements OnInit { + + constructor() { } + collapsed: boolean = false; + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/components/time-out-service/time-out-service.component.html b/frontend/src/app/components/time-out-service/time-out-service.component.html new file mode 100644 index 00000000..f09d1376 --- /dev/null +++ b/frontend/src/app/components/time-out-service/time-out-service.component.html @@ -0,0 +1,4 @@ +

+ Service Error! +

+Click to reload \ No newline at end of file diff --git a/frontend/src/app/components/time-out-service/time-out-service.component.scss b/frontend/src/app/components/time-out-service/time-out-service.component.scss new file mode 100644 index 00000000..db392424 --- /dev/null +++ b/frontend/src/app/components/time-out-service/time-out-service.component.scss @@ -0,0 +1,19 @@ +h2 { + text-align: center; + padding-top: 100px; + &::ng-deep { + .alert-danger { + font-size: 60px; + font-weight: bold; + display: inline-block; + line-height: 60px; + } + } +} +a { + display: block; + width: 300px; + font-size: 22px; + margin: 20px auto; + text-align: center; +} \ No newline at end of file diff --git a/frontend/src/app/components/time-out-service/time-out-service.component.spec.ts b/frontend/src/app/components/time-out-service/time-out-service.component.spec.ts new file mode 100644 index 00000000..c25a95c0 --- /dev/null +++ b/frontend/src/app/components/time-out-service/time-out-service.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TimeOutServiceComponent } from './time-out-service.component'; + +describe('TimeOutServiceComponent', () => { + let component: TimeOutServiceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TimeOutServiceComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimeOutServiceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/time-out-service/time-out-service.component.ts b/frontend/src/app/components/time-out-service/time-out-service.component.ts new file mode 100644 index 00000000..9d7e8379 --- /dev/null +++ b/frontend/src/app/components/time-out-service/time-out-service.component.ts @@ -0,0 +1,40 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { MessageService } from 'src/app/components/message/message.service'; + +@Component({ + selector: 'app-time-out-service', + templateUrl: './time-out-service.component.html', + styleUrls: ['./time-out-service.component.scss'] +}) +export class TimeOutServiceComponent implements OnInit { + + constructor(private $msg: MessageService, private router: Router) { } + + ngOnInit(): void { + } + + reload () { + const redirect = sessionStorage.getItem('lifecycleManager-redirect') + if (redirect) { + this.router.navigateByUrl(redirect) + setTimeout(() => { + sessionStorage.removeItem('lifecycleManager-redirect') + }, 1000) + } else { + this.router.navigateByUrl('/login') + this.$msg.warning('serverMessage.default401', 1000) + } + } +} diff --git a/frontend/src/app/pipes/shared/date-format.pipe.spec.ts b/frontend/src/app/pipes/shared/date-format.pipe.spec.ts new file mode 100644 index 00000000..b9c6144c --- /dev/null +++ b/frontend/src/app/pipes/shared/date-format.pipe.spec.ts @@ -0,0 +1,8 @@ +import { DateFormatPipe } from './date-format.pipe'; + +describe('DateFormatPipe', () => { + it('create an instance', () => { + const pipe = new DateFormatPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pipes/shared/date-format.pipe.ts b/frontend/src/app/pipes/shared/date-format.pipe.ts new file mode 100644 index 00000000..e35dcb23 --- /dev/null +++ b/frontend/src/app/pipes/shared/date-format.pipe.ts @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Pipe, PipeTransform } from '@angular/core'; + +import * as moment from 'moment' +@Pipe({ + name: 'dateFormat', + pure: false +}) +export class DateFormatPipe implements PipeTransform { + + //date-format pipe is to format date-stamp based on the selected language + transform(value: any, ...args: unknown[]): unknown { + let res = '' + const lang = localStorage.getItem('Lifecycle-Manager-Language') || 'en' + if (lang === 'zh_CN') { + res = moment(value).format('YYYY年MM月DD日 HH时mm分ss秒') + } else { + res = moment(value).format('ll, LTS') + } + return res; + } + +} diff --git a/frontend/src/app/pipes/shared/shared.module.ts b/frontend/src/app/pipes/shared/shared.module.ts new file mode 100644 index 00000000..ba8845a7 --- /dev/null +++ b/frontend/src/app/pipes/shared/shared.module.ts @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DateFormatPipe } from './date-format.pipe'; +import { TranslateModule } from '@ngx-translate/core'; +import { TemplateReplacementPipe } from './template-replacement.pipe'; + +@NgModule({ + declarations: [ + DateFormatPipe, + TemplateReplacementPipe, + ], + imports: [ + CommonModule, + TranslateModule + ], + exports: [DateFormatPipe, TranslateModule, TemplateReplacementPipe] +}) +export class SharedModule { } diff --git a/frontend/src/app/pipes/shared/template-replacement.pipe.spec.ts b/frontend/src/app/pipes/shared/template-replacement.pipe.spec.ts new file mode 100644 index 00000000..e37ccd4f --- /dev/null +++ b/frontend/src/app/pipes/shared/template-replacement.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TemplateReplacementPipe } from './template-replacement.pipe'; + +describe('TemplateReplacementPipe', () => { + it('create an instance', () => { + const pipe = new TemplateReplacementPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pipes/shared/template-replacement.pipe.ts b/frontend/src/app/pipes/shared/template-replacement.pipe.ts new file mode 100644 index 00000000..0858ca6b --- /dev/null +++ b/frontend/src/app/pipes/shared/template-replacement.pipe.ts @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'templateReplacement' +}) +export class TemplateReplacementPipe implements PipeTransform { + + transform(value: string, ...args: string[] | number[]): unknown { + if (typeof value !== 'string') { + throw new Error("The type of the value must be a string"); + } + for (let i = 0; i < args.length; i++) { + if (typeof args[i] === 'string' || typeof args[i] === 'number') { + value = value.replace(/d%/, args[i] + '') + } else { + throw new Error("The template replacement value type must be string or numeric"); + } + } + return value; + } + +} diff --git a/frontend/src/app/router-guard.ts b/frontend/src/app/router-guard.ts new file mode 100644 index 00000000..2a6d5d15 --- /dev/null +++ b/frontend/src/app/router-guard.ts @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; +import { AuthService } from './services/common/auth.service'; + +@Injectable() + +export class RouterGuard implements CanActivate { + constructor(private router: Router, private authService: AuthService) { + } + experimentEnabled = false + public async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + await new Promise((re, rj) => { + this.authService.getLCMServiceStatus() + .subscribe((data: any) => { + // use the value of 'experiment_enabled' to control enabling openfl related content or not + this.experimentEnabled = data?.experiment_enabled; + re(this.experimentEnabled) + }) + }) + return this.experimentEnabled + } +} \ No newline at end of file diff --git a/frontend/src/app/services/common/auth.service.spec.ts b/frontend/src/app/services/common/auth.service.spec.ts new file mode 100644 index 00000000..f1251cac --- /dev/null +++ b/frontend/src/app/services/common/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/common/auth.service.ts b/frontend/src/app/services/common/auth.service.ts new file mode 100644 index 00000000..e26b014c --- /dev/null +++ b/frontend/src/app/services/common/auth.service.ts @@ -0,0 +1,49 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + constructor(private http: HttpClient) { } + + login(username: string, password: string): Observable { + return this.http.post('/user/login', { + username, + password + }); + } + + logout(): Observable { + return this.http.post('/user/logout', { + }); + } + + getCurrentUser () { + return this.http.get('/user/current') + } + + changePassword(curPassword: string, newPassword: string, userId: any) : Observable { + return this.http.put('/user/'+String(userId)+'/password', { + "cur_password": curPassword, + "new_Password" : newPassword + }); + } + + getLCMServiceStatus() : Observable { + return this.http.get('/status') + } +} diff --git a/frontend/src/app/services/common/certificate.service.spec.ts b/frontend/src/app/services/common/certificate.service.spec.ts new file mode 100644 index 00000000..bc41de82 --- /dev/null +++ b/frontend/src/app/services/common/certificate.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CertificateMgService } from './certificate.service'; + +describe('CertificateMgService', () => { + let service: CertificateMgService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CertificateMgService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/common/certificate.service.ts b/frontend/src/app/services/common/certificate.service.ts new file mode 100644 index 00000000..c9404a7e --- /dev/null +++ b/frontend/src/app/services/common/certificate.service.ts @@ -0,0 +1,40 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { CertificateModel, CertificateAuthority} from 'src/app/view/certificate/certificate-model'; + +@Injectable({ + providedIn: 'root' +}) +export class CertificateMgService { + + constructor(private http: HttpClient) { } + getCertificateList() { + return this.http.get('/certificate'); + } + getCertificateAuthority() { + return this.http.get('/certificate-authority'); + } + createCertificateAuthority(info: CertificateAuthority) { + return this.http.post('/certificate-authority', info); + } + updateCertificateAuthority(uuid: string, info: CertificateAuthority) { + return this.http.put('/certificate-authority/' + uuid, info); + } + deleteCertificate(uuid: string) { + return this.http.delete('/certificate/' + uuid); + } + getEmbeddingCAConfig() { + return this.http.get('/certificate-authority/built-in-ca'); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/common/chart.service.spec.ts b/frontend/src/app/services/common/chart.service.spec.ts new file mode 100644 index 00000000..a3a12f61 --- /dev/null +++ b/frontend/src/app/services/common/chart.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ChartService } from './chart.service'; + +describe('ChartService', () => { + let service: ChartService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ChartService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/common/chart.service.ts b/frontend/src/app/services/common/chart.service.ts new file mode 100644 index 00000000..5e9859c1 --- /dev/null +++ b/frontend/src/app/services/common/chart.service.ts @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ChartService { + + constructor(private http: HttpClient) {} + + getChartList() { + return this.http.get('/chart'); + } + + getChartDetail(uuid:string) { + return this.http.get('/chart/'+uuid); + } +} diff --git a/frontend/src/app/services/common/endpoint.service.spec.ts b/frontend/src/app/services/common/endpoint.service.spec.ts new file mode 100644 index 00000000..68f4311a --- /dev/null +++ b/frontend/src/app/services/common/endpoint.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EndpointService } from './endpoint.service'; + +describe('EndpointService', () => { + let service: EndpointService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EndpointService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/common/endpoint.service.ts b/frontend/src/app/services/common/endpoint.service.ts new file mode 100644 index 00000000..b7b5bb16 --- /dev/null +++ b/frontend/src/app/services/common/endpoint.service.ts @@ -0,0 +1,65 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpUrlEncodingCodec } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class EndpointService { + + constructor(private http: HttpClient) { } + codec = new HttpUrlEncodingCodec; + + getEndpointList() { + return this.http.get('/endpoint'); + } + + getEndpointDetail(uuid:string) { + return this.http.get('/endpoint/'+uuid); + } + + deleteEndpoint(uuid:string, uninstall:boolean): Observable { + let params = new HttpParams().set('uninstall', uninstall); + return this.http.delete('/endpoint/'+uuid,{params: params}); + } + + checkEndpoint (uuid:string) { + return this.http.post('/endpoint/'+uuid+'/kubefate/check', {}); + } + postEndpointScan (uuid:string, type: string) { + return this.http.post('/endpoint/scan', { + infra_provider_uuid: uuid, + type: type + }); + } + getKubefateYaml(service_username: string, service_password: string, hostname: string, use_registry: boolean, registry: string, use_registry_secret: boolean, registry_server_url: string, registry_username: string, registry_password: string) { + let params = new HttpParams() + .set('service_username', service_username) + .set('service_password', service_password) + .set('hostname', hostname) + .set('use_registry', use_registry) + .set('registry', registry) + .set('use_registry_secret', use_registry_secret) + .set('registry_server_url', registry_server_url) + .set('registry_username', registry_username) + .set('registry_password', registry_password); + return this.http.get('/endpoint/kubefate/yaml',{params: params}); + } + + createEndpoint(endpointConfig:any) { + return this.http.post('/endpoint', endpointConfig); + } + +} diff --git a/frontend/src/app/services/common/infra.service.spec.ts b/frontend/src/app/services/common/infra.service.spec.ts new file mode 100644 index 00000000..8f8133cb --- /dev/null +++ b/frontend/src/app/services/common/infra.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { InfraService } from './infra.service'; + +describe('InfraService', () => { + let service: InfraService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InfraService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/common/infra.service.ts b/frontend/src/app/services/common/infra.service.ts new file mode 100644 index 00000000..8ebba044 --- /dev/null +++ b/frontend/src/app/services/common/infra.service.ts @@ -0,0 +1,51 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { InfraResponse } from 'src/app/view/infra/infra-model'; + +@Injectable({ + providedIn: 'root' +}) + +export class InfraService { + + constructor(private http: HttpClient) { } + + getInfraList() { + return this.http.get('/infra'); + } + + getInfraDetail(uuid:string) { + return this.http.get('/infra/'+uuid); + } + + deleteInfra(uuid:string): Observable { + return this.http.delete('/infra/'+uuid); + } + + createInfra(infraInfo:any): Observable { + return this.http.post('/infra', infraInfo); + } + + updateInfraProvider(infraInfo: any, uuid:string) : Observable { + return this.http.put('/infra/'+uuid, infraInfo) + } + + testK8sConnection(kubeconfig_content:any){ + return this.http.post('/infra/kubernetes/connect',{ + kubeconfig_content + }); + } + +} diff --git a/frontend/src/app/services/federation-fate/fed.service.spec.ts b/frontend/src/app/services/federation-fate/fed.service.spec.ts new file mode 100644 index 00000000..cbe75d63 --- /dev/null +++ b/frontend/src/app/services/federation-fate/fed.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FedService } from './fed.service'; + +describe('FedService', () => { + let service: FedService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FedService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/federation-fate/fed.service.ts b/frontend/src/app/services/federation-fate/fed.service.ts new file mode 100644 index 00000000..d78e847a --- /dev/null +++ b/frontend/src/app/services/federation-fate/fed.service.ts @@ -0,0 +1,114 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class FedService { + + constructor(private http: HttpClient) { } + + getFedList() { + return this.http.get('/federation'); + } + + getFedDetail(uuid:string) { + return this.http.get('/federation/fate/'+uuid); + } + + deleteFed(fed_uuid:string): Observable { + return this.http.delete('/federation/fate/'+fed_uuid); + } + + deleteParticipant(fed_uuid:string, type:string, uuid: string, forceRemove:boolean): Observable { + let params = new HttpParams().set('force', forceRemove); + return this.http.delete('/federation/fate/'+fed_uuid+'/'+type+'/'+uuid , {params: params}); + } + + createFed(infraInfo:any): Observable { + return this.http.post('/federation/fate/', infraInfo); + } + + getFedParticipantList(uuid:string) { + return this.http.get('/federation/fate/'+uuid+'/participant'); + } + + createExchange(fed_uuid:string, exchangeInfo:any): Observable { + return this.http.post('/federation/fate/'+ fed_uuid +'/exchange', exchangeInfo); + } + + getExchangeYaml(chart_uuid:string, namespace:string, name: string, service_type: number, registry: string, use_registry: boolean, use_registry_secret: boolean, enable_psp: boolean) { + let params = new HttpParams() + .set('chart_uuid', chart_uuid) + .set('namespace', namespace) + .set('name', name) + .set('service_type', service_type) + .set('registry', registry) + .set('use_registry', use_registry) + .set('use_registry_secret', use_registry_secret) + .set('enable_psp', enable_psp); + return this.http.get('/federation/fate/exchange/yaml',{params: params}); + } + + checkPartyID(fed_uuid:string, party_id:number): Observable { + let params = new HttpParams().set('party_id', party_id); + return this.http.post('/federation/fate/'+ fed_uuid +'/partyID/check', {},{params: params}); + } + + getClusterYaml(federation_uuid:string, chart_uuid:string, party_id:number, namespace:string, name:string, service_type: number, registry: string, use_registry: boolean, use_registry_secret: boolean, enable_persistence: boolean, storage_class: string, enable_psp: boolean) { + let params = new HttpParams() + .set('chart_uuid', chart_uuid) + .set('federation_uuid', federation_uuid) + .set('party_id', party_id) + .set('namespace', namespace) + .set('name', name) + .set('service_type', service_type) + .set('registry', registry) + .set('use_registry', use_registry) + .set('use_registry_secret', use_registry_secret) + .set('enable_persistence', enable_persistence) + .set('storage_class', storage_class) + .set('enable_psp', enable_psp); + return this.http.get('/federation/fate/cluster/yaml',{params: params}); + } + + createCluster(fed_uuid:string, clusterInfo:any): Observable { + return this.http.post('/federation/fate/'+ fed_uuid +'/cluster', clusterInfo); + } + + getClusterInfo (fed_uuid:string, cluster_uuid:string) { + return this.http.get(`/federation/fate/${fed_uuid}/cluster/${cluster_uuid}`) + } + + getExchangeInfo (fed_uuid:string, cluster_uuid:string) { + return this.http.get(`/federation/fate/${fed_uuid}/exchange/${cluster_uuid}`) + } + + deleteClusterInfo (fed_uuid:string, cluster_uuid:string) { + return this.http.delete(`/federation/fate/${fed_uuid}/cluster/${cluster_uuid}`) + } + + deleteExchangeInfo (fed_uuid:string, cluster_uuid:string) { + return this.http.delete(`/federation/fate/${fed_uuid}/exchange/${cluster_uuid}`) + } + + createExternalExchange(fed_uuid:string, externalExchange:any): Observable { + return this.http.post('/federation/fate/'+ fed_uuid +'/exchange/external', externalExchange); + } + + createExternalCluster(fed_uuid:string, externalCluster:any): Observable { + return this.http.post('/federation/fate/'+ fed_uuid +'/cluster/external', externalCluster); + } +} \ No newline at end of file diff --git a/frontend/src/app/services/openfl/openfl-model-type.ts b/frontend/src/app/services/openfl/openfl-model-type.ts new file mode 100644 index 00000000..e5195da8 --- /dev/null +++ b/frontend/src/app/services/openfl/openfl-model-type.ts @@ -0,0 +1,166 @@ +export interface TokenType { + "description": string, + "expired_at": string, + "labels": {[key:string]:string}, + "limit": number, + "name": string, + "token_str": string, + "uuid": string +} + +export interface OpenflType { + "created_at": string, + "description": string, + "domain": string, + "name": string, + "shard_descriptor_config": { + "envoy_config_yaml": string, + "python_files": {[key:string]:string}, + "sample_shape": string[], + "target_shape": string[] + }, + "type": string, + "use_customized_shard_descriptor": boolean, + "uuid": string +} + +export interface CreateTokenType { + "description": string, + "expired_at": string | Date, + "labels": {[key:string]:string}, + "limit": number, + "name": string +} + +export interface OpenflPsotModel { + "description": string, + "domain": string, + "name": string, + "shard_descriptor_config": { + "envoy_config_yaml": string, + "python_files": {[key:string]: string} + "sample_shape": string[], + "target_shape": string[] + }, + "use_customized_shard_descriptor": boolean +} + +export interface DirectorModel { + "chart_uuid": string, + "deployment_yaml": string, + "description": string, + "director_server_cert_info": { + "binding_mode": number + "common_name": string, + "uuid": string + }, + "endpoint_uuid": string, + "federation_uuid": string, + "jupyter_client_cert_info": { + "binding_mode": number, + "common_name": string, + "uuid": string + }, + "jupyter_password": string, + "name": string, + "namespace": string, + "registry_config": { + "registry": string, + "registry_secret_config": { + "password": string, + "server_url": string, + "username": string + }, + "use_registry": boolean, + "use_registry_secret": boolean + }, + "service_type": number +} +export interface ResponseModal { + "code": number + "data": any + "message": string +} +export interface LabelModel { + key:string + value:string + no?: number +} +export interface EnvoyModel { + "uuid": string +"name": string +"description": string +"created_at": string +"type": number +"endpoint_name": string +"endpoint_uuid": string +"infra_provider_name": string +"infra_provider_uuid": string +"namespace": string +"cluster_uuid": string +"status": number, +"access_info": any +"token_str": string +"token_name": string +"labels": {[key:string]:string}[], +"selected":boolean, +"deleteFailed":boolean, +"deleteSuccess":boolean, +"deleteSubmit":boolean, +"errorMessage":boolean, +showLabelListFlag?:boolean +} + +export interface DirectorInfoModel { + "access_info": {[key:string]:any} + "cluster_uuid": string, + "created_at": string, + "description": string, + "endpoint_name": string, + "endpoint_uuid": string, + "infra_provider_name": string, + "infra_provider_uuid": string, + "name": string, + "namespace": string, + "status": number, + "type": number, + "uuid": string +} + +export interface EnvoyInfoModel { + "access_info": { + "additionalProp1": { + "fqdn": string, + "host": string, + "port": number, + "service_type": string, + "tls": boolean + }, + "additionalProp2": { + "fqdn": string, + "host": string, + "port": number, + "service_type": string, + "tls": boolean + }, + "additionalProp3": { + "fqdn": string, + "host": string, + "port": number, + "service_type": string, + "tls": boolean + } + }, + "cluster_uuid": string, + "created_at": string, + "description": string, + "endpoint_name": string, + "endpoint_uuid": string, + "infra_provider_name": string, + "infra_provider_uuid": string, + "name": string, + "namespace": string, + "status": number, + "type": number, + "uuid": string +} \ No newline at end of file diff --git a/frontend/src/app/services/openfl/openfl.service.spec.ts b/frontend/src/app/services/openfl/openfl.service.spec.ts new file mode 100644 index 00000000..78d38677 --- /dev/null +++ b/frontend/src/app/services/openfl/openfl.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { OpenflService } from './openfl.service'; + +describe('OpenflService', () => { + let service: OpenflService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(OpenflService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/openfl/openfl.service.ts b/frontend/src/app/services/openfl/openfl.service.ts new file mode 100644 index 00000000..619d6d50 --- /dev/null +++ b/frontend/src/app/services/openfl/openfl.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { TokenType, OpenflType, CreateTokenType, OpenflPsotModel,DirectorModel,ResponseModal } from './openfl-model-type' +@Injectable({ + providedIn: 'root' +}) +export class OpenflService { + + constructor(private http: HttpClient) {} + + getOpenflFederationDetail (uuid:string) { + return this.http.get(`/federation/openfl/${uuid}`); + } + + deleteOpenflFederation (fed_uuid:string): Observable { + return this.http.delete(`/federation/openfl/${fed_uuid}`) + } + + getTokenList(uuid:string): Observable { + return this.http.get(`/federation/openfl/${uuid}/token`); + } + + createTokenInfo (fed_uuid:string, tokenInfo:any): Observable { + return this.http.post(`/federation/openfl/${fed_uuid}/token`, tokenInfo); + } + + deleteTokenInfo (fed_uuid:string, uuid:string) { + return this.http.delete(`/federation/openfl/${fed_uuid}/token/${uuid}`) + } + + createOpenflFederation(openflInfo:OpenflPsotModel): Observable { + return this.http.post('/federation/openfl', openflInfo); + } + + createDirector (openflId:string,direcortInfo:DirectorModel): Observable { + return this.http.post(`/federation/openfl/${openflId}/director`, direcortInfo) + } + + deleteDirector (fed_uuid:string, director_uuid:string, forceRemove: boolean): Observable { + return this.http.delete(`/federation/openfl/${fed_uuid}/director/${director_uuid}?force=${forceRemove}`) + } + + getDirectorYaml( + federation_uuid:string, + jupyter_password:string, + chart_uuid:string, + namespace:string, + name: string, + service_type: number, + registry: string, + use_registry: boolean, + use_registry_secret: boolean, + enable_psp: boolean + ): Observable { + let params = new HttpParams() + .set('chart_uuid', chart_uuid) + .set('federation_uuid', federation_uuid) + .set('namespace', namespace) + .set('name', name) + .set('service_type', service_type) + .set('jupyter_password', jupyter_password) + .set('registry', registry) + .set('use_registry', use_registry) + .set('use_registry_secret', use_registry_secret) + .set('enable_psp', enable_psp); + return this.http.get('/federation/openfl/director/yaml',{params: params}); + } + + getParticipantInfo (fed_uuid:string): Observable { + return this.http.get(`/federation/openfl/${fed_uuid}/participant`); + } + + getDirectorInfo (fed_uuid: string, director_uuid: string) : Observable { + return this.http.get(`/federation/openfl/${fed_uuid}/director/${director_uuid}`); + } + + getEnvoyInfo (fed_uuid: string, envoy_uuid: string) : Observable { + return this.http.get(`/federation/openfl/${fed_uuid}/envoy/${envoy_uuid}`); + } + + deleteEnvoy (fed_uuid:string, envoy_uuid:string, forceRemove: boolean): Observable { + return this.http.delete(`/federation/openfl/${fed_uuid}/envoy/${envoy_uuid}?force=${forceRemove}`) + } +} diff --git a/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.html b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.html new file mode 100644 index 00000000..c6359333 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.html @@ -0,0 +1,120 @@ +
+ <<{{'CommonlyUse.back'| translate}} +

{{'Certificate.authority'| translate}} + {{constantGather('caStatus', + caStatus).name | translate}} +

+ + {{'CertificateDetail.statusErrorAlert'|translate}}
{{caStatusMessage}} +
+
+
+ + +
+
+
    +
  • + + + + + {{form.get('name')?.errors?.emptyMessage || form.get('name')?.errors?.message | translate}} + + {{'CommonlyUse.few' | + translate}}{{form.get('name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' | + translate}} + {{'CommonlyUse.many' | + translate}}{{form.get('name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' | + translate}} + +
  • +
  • + + + + +
  • + + + + + + + {{'validator.empty' | translate}} + + + + + {{'CertificateDetail.choose'|translate}} + + + + + + + + + + + + {{errorMessage}} + + + + + + + {{'validator.internet' | translate}} + + + + + + {{'validator.empty' | translate}} + + + + + {{'validator.empty' | translate}} + + + + + {{'validator.empty' | translate}} + +
+
+
+
+ + + + +
+
+ \ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.scss b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.scss new file mode 100644 index 00000000..5a2eff40 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.scss @@ -0,0 +1,80 @@ + +.card { + margin-top: 3px; + min-width: 1200px; + .clr-form::ng-deep { + .list { + padding: 12px 18px; + list-style: none; + width: 110%; + label { + font-size: 14px; + } + .clr-form-control { + margin-top: 0px; + height: 50px; + .clr-input { + width: 330px; + &.passwd { + width: 300px; + } + } + &.clr-form-control-disabled { + label { + color: #333; + } + } + } + .clr-textarea-block { + display: block; + height: auto; + textarea { + width: 650px; + height: 300px; + } + } + .clr-control-container { + .clr-input-wrapper { + height: 36px; + max-height: 36px; + .clr-input-group { + height: 36px; + } + } + } + .pemlabel { + width: 100%; + } + } + } +} +.pageLoading { + min-width: 20px; + min-height: 20px; + width: 20px; + height: 20px; + top: 5px; +} +.t2{ + width: 330px; + height: 60px; +} +.textarea-li { + height: 80px; +} + +.embeddingalert { + width: 600px; + height: fit-content; + .embeddingerroricon { + float: left; + } +} +.statusLabel { + margin-bottom: 12px; +} + +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} diff --git a/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.spec.ts b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.spec.ts new file mode 100644 index 00000000..1c6ee66c --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CertificateAuthorityDetailComponent } from './certificate-authority-detail.component'; + +describe('CertificateDetailComponent', () => { + let component: CertificateAuthorityDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CertificateAuthorityDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CertificateAuthorityDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.ts b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.ts new file mode 100644 index 00000000..2cc09ba9 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-authority-detail/certificate-authority-detail.component.ts @@ -0,0 +1,268 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { ValidatorGroup } from 'src/utils/validators' +import { ActivatedRoute, Router } from '@angular/router' +import { CertificateMgService } from 'src/app/services/common/certificate.service' +import { constantGather } from 'src/utils/constant' + +@Component({ + selector: 'app-certificate-detail', + templateUrl: './certificate-authority-detail.component.html', + styleUrls: ['./certificate-authority-detail.component.scss'] +}) +export class CertificateAuthorityDetailComponent implements OnInit { + //form is for Certificate Authority configuration information + form = this.fb.group( + ValidatorGroup([ + { + name: 'name', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'type', + type: [''], + value: 1 + }, + { + name: 'embedding', + value: '', + type: [''], + }, + { + name: 'url', + value: '', + type: ['internet'] + }, + { + name: 'provisionerName', + value: '', + type: [''] + }, + { + name: 'provisionerPassword', + value: '', + type: [''] + }, + { + name: 'pem', + type: [''] + } + ]) + + ) + isAdd = false + isUpdate = false + uuid = '' + isShowDetailFailed = false + errorMessage = '' + isUpdateBtn = true + submiting = false + constructor(private fb: FormBuilder, private route: ActivatedRoute, private certificateService: CertificateMgService, private router: Router) { } + + ngOnInit(): void { + this.route.params.subscribe(p => { + if (p.id && p.id === 'new') { + this.isAdd = true + //enable form to edit + this.setDisabled(false) + } else { + this.uuid = p.id + this.isAdd = false + //disable form to edit + this.setDisabled(true) + this.getCAConfig() + } + }) + } + constantGather = constantGather + + isGetEmbeddingCAConfigFailed = false; + //getEmbeddingCAConfig is for get the built-in CA config info if lifecycle manager is deployed with step-ca service + getEmbeddingCAConfig() { + this.isGetEmbeddingCAConfigFailed = false; + this.certificateService.getEmbeddingCAConfig().subscribe( + data => { + if (data.data) { + this.form.controls["provisionerName"].setValue(data.data.provisioner_name) + this.form.controls["provisionerPassword"].setValue(data.data.provisioner_password) + this.form.controls["pem"].setValue(data.data.service_cert_pem) + this.form.controls["url"].setValue("https://" + data.data.service_url + ":9000") + } + }, + err => { + this.isGetEmbeddingCAConfigFailed = true + this.errorMessage = err.error.message + } + ) + } + + //onEmbeddingChange is to reset the CA config form to when selection is changed + onEmbeddingChange() { + this.isGetEmbeddingCAConfigFailed = false; + if (this.form.get("embedding")?.value === "embedding") { + this.getEmbeddingCAConfig() + } else { + if (this.isAdd) { + this.form.controls["provisionerName"].setValue('') + this.form.controls["provisionerPassword"].setValue('') + this.form.controls["pem"].setValue('') + this.form.controls["url"].setValue('') + } else { + this.form.controls["provisionerName"].setValue(this.caDetail?.provisionerName) + this.form.controls["provisionerPassword"].setValue(this.caDetail?.provisionerPassword) + this.form.controls["pem"].setValue(this.caDetail?.pem) + this.form.controls["url"].setValue(this.caDetail?.url) + } + } + } + get useStepCAType() { + return this.form.get('type')?.value === "1" + } + + get updateFlag() { + return this.isUpdate + } + + set updateFlag(val) { + if (val) { + //enable form to update + this.setDisabled(false) + } + this.isUpdate = val + } + get submit() { + if (this.isAdd) { + return this.form.valid + } else { + if (this.updateFlag) { + return this.form.valid + } else { + return false + } + } + } + //setDisabled is to set CA config form disabled or not + setDisabled(opt: boolean) { + for (const key in this.form.controls) { + const val = this.form.get(key) + if (val) { + if (opt) { + val.disable() + } else { + val.enable() + } + } + } + } + caDetail: any + caStatus = 0 + caStatusMessage = "" + getCaDetailFailed = false; + isPageLoading = false; + //getCAConfig is to getting the CA config info and fill the form with + getCAConfig() { + this.isPageLoading = true; + this.getCaDetailFailed = false; + this.certificateService.getCertificateAuthority().subscribe( + data => { + if (data.data) { + const value = { + provisionerName: data.data.config.provisioner_name, + provisionerPassword: data.data.config.provisioner_password, + pem: data.data.config.service_cert_pem, + url: data.data.config.service_url, + description: data.data.description, + name: data.data.name, + type: data.data.type + '', + embedding: "" + } + this.form.setValue(value) + this.caDetail = value + this.caStatus = data.data.status + this.caStatusMessage = data.data.status_message + this.isPageLoading = false; + } + }, + err => { + this.getCaDetailFailed = true; + this.isPageLoading = false; + } + ) + } + //submitCAConfig is to create/update the CA configuration + submitCAConfig(value: any) { + this.submiting = true + const data = { + config: { + provisioner_name: value.provisionerName?.trim(), + provisioner_password: value.provisionerPassword?.trim(), + service_cert_pem: value.pem?.trim(), + service_url: value.url?.trim() + }, + description: value.description, + name: value.name, + type: value.type * 1 + } + if (this.isAdd) { + this.certificateService.createCertificateAuthority(data).subscribe( + data => { + this.isUpdateBtn = true + this.updateFlag = false + this.isAdd = false + this.setDisabled(true) + this.submiting = false + this.isShowDetailFailed = false + this.router.navigateByUrl('/certificate') + }, + err => { + this.isShowDetailFailed = true + this.errorMessage = err.error.message + this.submiting = false + } + ) + } else { + this.certificateService.updateCertificateAuthority(this.uuid, data).subscribe( + data => { + this.isUpdateBtn = true + this.updateFlag = false + this.isAdd = false + this.setDisabled(true) + this.submiting = false + this.isShowDetailFailed = false + this.reloadCurrentRoute() + }, + err => { + this.isShowDetailFailed = true + this.errorMessage = err.error.message + this.submiting = false + } + ) + } + } + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} +interface AbstractControl { onlySelf?: boolean | undefined; emitEvent?: boolean | undefined; } diff --git a/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.html b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.html new file mode 100644 index 00000000..f27c6f23 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.html @@ -0,0 +1,70 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'CertificateDetail.name'|translate}}

+ +
+ +
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{certificateDetail.name}} +
  • +
  • + {{'CertificateDetail.commonName'|translate}}: + {{certificateDetail.common_name}} +
  • +
  • + {{'CertificateDetail.expirationDate'|translate}}: + {{certificateDetail?.expiration_date | date :'medium'}} +
  • +
  • + {{'CertificateDetail.serialNumber'|translate}}: + {{certificateDetail.serial_number}} +
  • +
  • + UUID: + {{certificateDetail.uuid}} +
  • +
+
+
+
+
{{'CertificateDetail.bind'|translate}}:
+
+ + {{'CertificateDetail.participantName'|translate}} + {{'CertificateDetail.participantUuid'|translate}} + {{'CertificateDetail.federationName'|translate}} + {{'CertificateDetail.federationType'|translate}} + {{'CertificateDetail.serviceDescription'|translate}} + {{'CertificateDetail.serviceType'|translate}} + + + {{bind.participant_name}} + + {{bind.participant_uuid}} + {{bind.federation_name}} + + {{bind.federation_type}} + {{bind.service_description}} + {{constantGather('cerificateType', + bind.service_type).name | translate}} + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.scss b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.scss new file mode 100644 index 00000000..f749f8cb --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.scss @@ -0,0 +1,172 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } +} +.pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255,255,255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; +} +.card1 { + width: 98%; + min-width: 1200px; + ol { + h6 { + margin: 0px; + } + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + b { + display: inline-block; + width: 200px; + } + } + label { + width: 300px; + } + .clr-form-control { + margin-top: 5px; + .clr-textarea-wrapper { + width: 600px; + } + textarea { + width: 600px; + height: 300px; + } + } + } + &.ol { + li { + border-bottom: 1px solid #999; + padding-bottom: 10px; + } + } + &.ol-ul { + margin-left: 30px; + li { + span { + width: 120px; + font-size: 12px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + &.child { + font-size: 12px; + } + + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 300px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} +.card { + .alert { + width: 30%; + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + display: inline-block; + // width: 200px; + &.bind-mode { + width: 115px; + } + } + .clr-form-control { + margin-top: 5px; + textarea { + width: 600px; + height: 300px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + + } +} +.card3 { + width: 100%; + p { + font-size: 13px; + font-weight: 700; + margin-top: 10px; + margin-left: 30px; + } + .list { + // list-style: none; + margin-left: 20px; + padding-top: 0px; + li { + line-height: 24px; + height: 24px; + } + span { + font-size: 12px; + } + } + +} +.deployment-yaml { + color: #333; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.spec.ts b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.spec.ts new file mode 100644 index 00000000..d48c6489 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CertificateDetailComponent } from './certificate-detail.component'; + +describe('CertificateDetailComponent', () => { + let component: CertificateDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CertificateDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CertificateDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.ts b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.ts new file mode 100644 index 00000000..9eb60bb0 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-detail/certificate-detail.component.ts @@ -0,0 +1,64 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CertificateMgService } from 'src/app/services/common/certificate.service' +import { CertificateType } from '../certificate-model'; +import { ActivatedRoute } from '@angular/router' +import { constantGather, CerificateServiceType } from 'src/utils/constant'; +@Component({ + selector: 'app-certificate-detail', + templateUrl: './certificate-detail.component.html', + styleUrls: ['./certificate-detail.component.scss'] +}) +export class CertificateDetailComponent implements OnInit { + + constructor(private certificateService: CertificateMgService, private route: ActivatedRoute) { } + isPageLoading = true + isShowDetailFailed = false + errorMessage = "Service Error!" + cerificateType = CerificateServiceType + constantGather = constantGather + certificateDetail: CertificateType = { + name: '', + serial_number: '', + expiration_date: '', + common_name: '', + uuid: '', + bindings: [], + deleteFailed: false, + deleteSuccess: false, + deleteSubmit: false, + errorMessage: "" + } + private uuid: string = '' + ngOnInit(): void { + this.route.params.subscribe(value => { + this.uuid = value.id + this.getCertificateDetail() + }) + } + //getCertificateDetailis to get the certificate detail info + getCertificateDetail() { + this.isPageLoading = true; + //firstly get the certificate list due to there is no 'get certificate datail' API + this.certificateService.getCertificateList().subscribe( + (data: { data: any[]; }) => { + if (data.data) { + this.isPageLoading = false; + //find the spcific certificate information by certificate UUID + this.certificateDetail = data.data.find((el: any) => el.uuid === this.uuid) + if (!this.certificateDetail) this.errorMessage = "record not found" + } + } + ) + } +} diff --git a/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.html b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.html new file mode 100644 index 00000000..f5874696 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.html @@ -0,0 +1,234 @@ +
+
+

{{'Certificate.name'|translate}}

+ + + + {{errorMessage}} + + +
+ + + {{'Certificate.noCertificateAuthority'| + translate}} +
+
+ {{'Certificate.authority'| translate}} +
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{ca.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{ca.description}} +
  • +
  • + {{'CommonlyUse.type'|translate}}: + {{constantGather('caType', ca.type).name | translate}} +
  • +
  • + {{'CertificateDetail.serviceURL'|translate}}: + {{ca.config.service_url}} +
  • +
  • + {{'CertificateDetail.provisionerName'|translate}}: + {{ca.config.provisioner_name}} +
  • +
  • + {{'CommonlyUse.status'|translate}}: + {{constantGather('caStatus', + ca.status).name | translate}} +
  • +
+
+ +
+
+ +

+ {{'Certificate.names'|translate}} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + {{'CommonlyUse.name'|translate}} + + + {{'Certificate.commonName'|translate}}{{'Certificate.expirationDate'| translate}}{{'Certificate.serialNumber'|translate}}{{'Certificate.bindings'| translate}}
+ + {{certificate.name}}{{certificate.common_name}}{{certificate.expiration_date | dateFormat}}{{certificate.serial_number}} + + + + + + + + + + + + + + + + + + + +
+ {{'CertificateDetail.federationName'|translate}} + + {{'CertificateDetail.participantUuid'|translate}} + + {{'CertificateDetail.serviceDescription'|translate}} + + {{'CertificateDetail.serviceType'|translate}} +
{{bind.participant_name}}{{bind.participant_uuid}}{{bind.service_description}}{{constantGather('cerificateType', bind.service_type).name}}
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.scss b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.scss new file mode 100644 index 00000000..b370978a --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.scss @@ -0,0 +1,184 @@ +input { + width: 99%; +} +select{ + width: 150px; +} +textarea { + width: 100%; + height: 50px; +} +clr-date-container { + input { + width: 115px; + } +} +.sub { + margin-bottom: 12px; +} +table { + margin-top: 12px; + min-width: 1300px; + thead { + border-bottom: 1px solid #ccc; + tr { + background-color: #fafafa; + border-bottom: 1px solid #999; + th { + flex: 1; + border: none; + padding: 8px 0px 8px 12px; + min-width: 120px; + &.checkbox-item { + width: 40px; + min-width: 40px; + padding: 0px; + margin: 0px; + flex: 0; + input { + width: 40px; + } + } + span { + display: flex; + flex: 1; + padding: 3px 0; + border-right: 1px solid #999; + justify-content: space-between; + padding-right: 10px; + .sort { + cursor: pointer; + &.down { + transform: rotate(180deg); + } + } + } + &:last-of-type { + flex: 0; + min-width: 100px; + width: 100px; + span { + border-right: none; + } + } + &:nth-of-type(7) { + flex: 0; + min-width: 100px; + width: 100px; + } + } + } + } + tbody { + tr{ + border-bottom: 1px solid #999; + td { + &.checkbox-item { + width: 40px; + min-width: 40px; + padding: 0px; + margin: 0px; + flex: 0; + input { + width: 40px; + } + } + &:nth-of-type(7) { + flex: 0; + min-width: 100px; + width: 100px; + text-align: center; + } + &:last-of-type { + flex: 0; + min-width: 100px; + width: 100px; + } + &:nth-of-type(5) { + white-space: pre-wrap; + word-wrap: break-word; + } + } + td::ng-deep { + flex: 1; + border: none; + padding: 8px 12px; + min-width: 120px; + text-align: left; + .signpost { + .signpost-content { + max-width: none; + width: 600px; + } + .signpost-content-body { + padding: 10px; + max-width: none; + width: 600px; + } + } + &.title { + border-right: 1px solid #999; + color: #333; + padding-left: 0px; + padding-right: 0px; + } + .signpost-conten{ + max-width: none; width: 600px; transform: translateX(1016px) translateY(385px); + } + } + + } + } +} +.signpost-content-body { + padding: 10px; +} + +tr { + display: flex; + flex: 1; + align-items: center; + &.none { + display: block; + text-align: center; + padding-bottom: 20px; + } +} +.access_info { + width: 300px; + height: 150px; + border: none; +} + + +.card { + width: 700px; + .list { + input { + width: 100%; + } + select{ + width: 150px; + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } +} + +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} + +.statusLabel { + margin-bottom: 8px; + } \ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.spec.ts b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.spec.ts new file mode 100644 index 00000000..90b8e712 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CertificateMgComponent } from './certificate-mg.component'; + +describe('CertificateMgComponent', () => { + let component: CertificateMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CertificateMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CertificateMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.ts b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.ts new file mode 100644 index 00000000..b82c1735 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-mg/certificate-mg.component.ts @@ -0,0 +1,254 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { CertificateMgService } from 'src/app/services/common/certificate.service' +import { CertificateType } from '../certificate-model'; +import { constantGather } from 'src/utils/constant' +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + selector: 'app-certificate-mg', + templateUrl: './certificate-mg.component.html', + styleUrls: ['./certificate-mg.component.scss'] +}) +export class CertificateMgComponent implements OnInit { + constantGather = constantGather + certificatelist: CertificateType[] = []; + uuid = '' + ca: any + openModal: boolean = false; + selected: any = []; + + type: string = ''; + institution: string = ''; + authority: string = ''; + note: string = ''; + startdate: number = Date.now();; + enddate: number = 0; + today: number = Date.now(); + newCertificateForm = this.fb.group({ + type: [''], + name: [''], + authority: [''], + note: [''], + startdate: [''], + enddate: [''], + seriesNumber: [''], + server: [''] + }); + + openDeleteModal = false + errorMessage = "Service Error!" + isPageLoading = true; + + constructor(private fb: FormBuilder, private certificateService: CertificateMgService, private router: Router, private route: ActivatedRoute) { } + + ngOnInit(): void { + this.getCAinfo(); + this.getCertificateList(); + + } + onOpenModal() { + this.openModal = true; + } + + //Currently, 'Add a new certificate' is not supported + // resetModal() { + // this.newCertificateForm.reset(); + // this.openModal = false; + // } + + //getCAinfo is to get certificate authority information + getCAFailed = false + getCAinfo() { + this.isPageLoading = true + this.getCAFailed = false + this.certificateService.getCertificateAuthority().subscribe( + data => { + if (data.data) this.ca = data.data + this.isPageLoading = false; + }, + err => { + this.isPageLoading = false + this.getCAFailed = true + if (err.error.message) this.errorMessage = err.error.message + } + ) + } + + //getCertificateList is to get certificate list + getCertificateListFailed = false + getCertificateList() { + this.getCertificateListFailed = false + this.certificateService.getCertificateList().subscribe( + data => { + if (data.data) { + this.certificatelist = data.data.map(el => { + el.select = false + return el + }) + } else { + this.certificatelist = [] + } + }, + err => { + this.isPageLoading = false; + this.getCertificateListFailed = true + if (err.error.message) this.errorMessage = err.error.message + } + ) + } + + nameSortFlag = false + nameSort(type: string) { + this.nameSortFlag = !this.nameSortFlag + if (this.nameSortFlag) { + this.certificatelist.sort(reverse(type)) + } else { + this.certificatelist.sort(order(type)) + } + } + + //get allSelect is to get the selected list + get allSelect() { + if (this.certificatelist.length > 0) { + return this.certificatelist.every(el => el.select === true) + } else { + return false + } + } + //set allSelect is to select all certificate + set allSelect(val) { + this.certificatelist.forEach(el => this.checkCertificateBindings(el) ? el.select = val : el.select = false) + } + + //disableAllSelect is the flag to disable "select all" checkbox + get disableAllSelect() { + if (this.certificatelist.length > 0) return this.certificatelist.every(el => el.bindings.length > 0) + return true + } + + //openDeleteConfrimModal is to initial variables when open the modal of 'Delete Certificate' + openDeleteConfrimModal() { + this.openDeleteModal = true + this.isDeleteCertificateAllSuccess = false + } + + //deleteSelectedCertificat is to delete the selected certificate + async deleteSelectedCertificate() { + if (this.selectedCertificateList?.length > 0) { + await this.submitDeleteCertificate() + if (this.deleteCertificateAllSuccess()) this.reloadCurrentRoute() + } + } + + // submitDeleteCertificate is to submit http request for deletion with synchronization + async submitDeleteCertificate() { + for (let certificate of this.selectedCertificateList) { + certificate.deleteFailed = false + certificate.select = !certificate.deleteSuccess + certificate.deleteSubmit = true + if (!certificate.deleteSuccess && certificate.select) { + await this.certificateService.deleteCertificate(certificate.uuid) + .toPromise().then(() => { + certificate.deleteFailed = false; + certificate.deleteSuccess = true; + }, + err => { + this.openDeleteModal = true + certificate.deleteFailed = true; + certificate.deleteSuccess = false; + certificate.errorMessage = err.error.message + }); + } + } + } + + //checkCertificateBindings is to check if the certificate contains binding participants + checkCertificateBindings(certificate: CertificateType): boolean { + if (certificate) { + return !(certificate?.bindings?.length > 0) + } + return false + } + + get selectedCertificateList() { + const selectedList: CertificateType[] = [] + this.certificatelist.forEach(el => { + el.select ? selectedList.push(el) : null + }) + return selectedList + } + + get deleteBtnDisabled() { + return !(this.selectedCertificateList?.length > 0) + } + + isDeleteCertificateAllSuccess = false; + //deleteCertificateAllSuccess is to return if multiple deletion are all successful + deleteCertificateAllSuccess() { + for (const certificate of this.selectedCertificateList) { + if (!certificate.deleteSuccess) { + this.isDeleteCertificateAllSuccess = false + return this.isDeleteCertificateAllSuccess + } + } + this.isDeleteCertificateAllSuccess = true + return this.isDeleteCertificateAllSuccess + } + + //toDetail is redirect to certificate or CA detail page + toDetail(uuid: string, type?: boolean) { + if (type) { + this.router.navigate(['/certificate/detail/', uuid]); + } else { + this.router.navigate(['/certificate/authority', uuid]); + } + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} +// positive order function +function order(propertyName: string) { + return function (obj1: any, obj2: any) { + var value1 = obj1[propertyName]; + var value2 = obj2[propertyName]; + if (value1 < value2) { + return -1; + } else if (value1 > value2) { + return 1; + } else { + return 0; + } + } +} +// positive reverse function +function reverse(propertyName: string) { + return function (obj1: any, obj2: any) { + var value1 = obj1[propertyName]; + var value2 = obj2[propertyName]; + if (value1 < value2) { + return 1; + } else if (value1 > value2) { + return -1; + } else { + return 0; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/view/certificate/certificate-model.ts b/frontend/src/app/view/certificate/certificate-model.ts new file mode 100644 index 00000000..2e1789a1 --- /dev/null +++ b/frontend/src/app/view/certificate/certificate-model.ts @@ -0,0 +1,50 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface CertificateModel { + code: number, + data: CertificateType[], + message: string +} + +export interface CertificateType { + bindings: Binding[] + common_name: string, + expiration_date: string, + name: string, + serial_number: string, + uuid: string, + select?: boolean, + deleteFailed:boolean, + deleteSuccess:boolean, + deleteSubmit:boolean, + errorMessage: string +} +export interface Binding { + participant_name: string, + participant_uuid: string, + participant_type: string, + service_description: string, + service_type: number, + federation_type: string, + federation_uuid: string, +} +export interface CertificateAuthority { + config: { + provisioner_name: string + provisioner_password: string + service_cert_pem: string + service_url: string + }, + description: string, + name: string + type: number +} \ No newline at end of file diff --git a/frontend/src/app/view/chart/chart-detail/chart-detail.component.html b/frontend/src/app/view/chart/chart-detail/chart-detail.component.html new file mode 100644 index 00000000..9e5ad566 --- /dev/null +++ b/frontend/src/app/view/chart/chart-detail/chart-detail.component.html @@ -0,0 +1,98 @@ +
+ <<{{'CommonlyUse.back'| translate}} +

{{'ChartDetail.name'|translate}}

+ + + {{errorMessage}} + + +
+
+
+ +
+
+
    +
  • + {{'CommonlyUse.name'| translate}}: + {{chartDetail.name}} +
  • +
  • + {{'Chart.helmChartName'|translate}}: + {{chartDetail.chart_name}} +
  • +
  • + {{'CommonlyUse.description'| translate}}: + {{chartDetail.description}} +
  • +
  • + {{'CommonlyUse.type'| translate}}: + {{constantGather('charttype', chartDetail.type).name | translate}} +
  • +
  • + {{'CommonlyUse.creationTime'| translate}}: + {{chartDetail.created_at | dateFormat}} +
  • +
  • + {{'Chart.helmChartVersion'| translate}}: + {{chartDetail.version}} +
  • +
+
+
+
    +
  • +

    Chart.yaml:

    + +
  • +
  • +

    {{'ChartDetail.values'| translate}}:

    + +
  • +
    +
  • +

    {{'ChartDetail.template'| translate}}:

    + +
  • +
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/frontend/src/app/view/chart/chart-detail/chart-detail.component.scss b/frontend/src/app/view/chart/chart-detail/chart-detail.component.scss new file mode 100644 index 00000000..bff47e61 --- /dev/null +++ b/frontend/src/app/view/chart/chart-detail/chart-detail.component.scss @@ -0,0 +1,60 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } + } + .pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255,255,255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; + } + + +.card { + width: 100%; + min-width: 900px; + .list { + width: 100%; + input { + width: 100%; + } + select{ + width: 150px; + } + li { + &.codemirror::ng-deep { + .clr-textarea-wrapper { + height: 600px; + width: 600px; + } + } + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 200px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 800px; + height: 600px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} \ No newline at end of file diff --git a/frontend/src/app/view/chart/chart-detail/chart-detail.component.spec.ts b/frontend/src/app/view/chart/chart-detail/chart-detail.component.spec.ts new file mode 100644 index 00000000..318effc6 --- /dev/null +++ b/frontend/src/app/view/chart/chart-detail/chart-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartDetailComponent } from './chart-detail.component'; + +describe('ChartDetailComponent', () => { + let component: ChartDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChartDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChartDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/chart/chart-detail/chart-detail.component.ts b/frontend/src/app/view/chart/chart-detail/chart-detail.component.ts new file mode 100644 index 00000000..b036d5d0 --- /dev/null +++ b/frontend/src/app/view/chart/chart-detail/chart-detail.component.ts @@ -0,0 +1,75 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ChartService } from 'src/app/services/common/chart.service'; +import { CHARTTYPE, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-chart-detail', + templateUrl: './chart-detail.component.html', + styleUrls: ['./chart-detail.component.scss'] +}) +export class ChartDetailComponent implements OnInit { + + constructor(private chartservice: ChartService, private router: Router, private route: ActivatedRoute) { } + + ngOnInit(): void { + this.showChartDetail(); + } + chartType = CHARTTYPE; + constantGather = constantGather; + + uuid = String(this.route.snapshot.paramMap.get('id')); + chartDetail: any; + errorMessage = "Service Error!" + isShowChartDetailFailed: boolean = false + isPageLoading: boolean = true + values: any + about: any + valueTemplate: any + //createCodeMirror is to initial the yaml editor window + createCodeMirror(id: string, data: string, key: 'values' | 'about' | 'valueTemplate') { + const yamlHTML = document.getElementById(id) as any + this[key] = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + this[key].setValue(data) + + } + //showChartDetail is to get the chart detail information + showChartDetail() { + this.isPageLoading = true + this.isShowChartDetailFailed = false + this.chartservice.getChartDetail(this.uuid) + .subscribe((data: any) => { + this.chartDetail = data.data; + this.createCodeMirror('values', this.chartDetail.values, 'values') + this.createCodeMirror('about', this.chartDetail.about, 'about') + this.createCodeMirror('values_template', this.chartDetail.values_template, 'valueTemplate') + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isPageLoading = false + this.isShowChartDetailFailed = true + } + ); + } + +} diff --git a/frontend/src/app/view/chart/chart-mg/chart-mg.component.html b/frontend/src/app/view/chart/chart-mg/chart-mg.component.html new file mode 100644 index 00000000..5774c5ba --- /dev/null +++ b/frontend/src/app/view/chart/chart-mg/chart-mg.component.html @@ -0,0 +1,78 @@ +
+
+

{{'Chart.name'|translate}}

+ + + {{errorMessage}} + + + + +
+ +
+
+ +
+ + + +
+ {{'CommonlyUse.name'|translate}} + {{'Chart.helmChartName'|translate}} + {{'CommonlyUse.description'|translate}} + {{'CommonlyUse.type'|translate}} + {{'Chart.helmChartVersion'|translate}} + {{'CommonlyUse.creationTime'|translate}} + + + {{chart.name}} + {{chart.chart_name}} + {{chart.description}} + {{constantGather('charttype', chart.type).name | translate}} + {{chart.version}} + {{chart.created_at | dateFormat}} + + + {{chartlist ? chartlist.length : 0}} item(s) +
+
+ + + \ No newline at end of file diff --git a/frontend/src/app/view/chart/chart-mg/chart-mg.component.scss b/frontend/src/app/view/chart/chart-mg/chart-mg.component.scss new file mode 100644 index 00000000..da0bb655 --- /dev/null +++ b/frontend/src/app/view/chart/chart-mg/chart-mg.component.scss @@ -0,0 +1,19 @@ +input { + width: 99%; +} +select{ + width: 150px; +} +.t2{ + width: 100%; + height: 60px; +} +.fileIcon { + margin-top: -20px; + margin-left: 64px; +} +.refreshbtn { + float: right; + margin-right: 12px; + margin-top: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/view/chart/chart-mg/chart-mg.component.spec.ts b/frontend/src/app/view/chart/chart-mg/chart-mg.component.spec.ts new file mode 100644 index 00000000..85d1c66d --- /dev/null +++ b/frontend/src/app/view/chart/chart-mg/chart-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChartMgComponent } from './chart-mg.component'; + +describe('ChartMgComponent', () => { + let component: ChartMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChartMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChartMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/chart/chart-mg/chart-mg.component.ts b/frontend/src/app/view/chart/chart-mg/chart-mg.component.ts new file mode 100644 index 00000000..a64379fa --- /dev/null +++ b/frontend/src/app/view/chart/chart-mg/chart-mg.component.ts @@ -0,0 +1,78 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import '@cds/core/file/register.js'; +import { ChartService } from 'src/app/services/common/chart.service'; +import { CHARTTYPE, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-chart-mg', + templateUrl: './chart-mg.component.html', + styleUrls: ['./chart-mg.component.scss'] +}) +export class ChartMgComponent implements OnInit { + + selectedChartList: any = []; + openModal: boolean = false; + newchartForm = this.fb.group({ + name: [''], + type: [''], + description: [''] + }); + constructor(private fb: FormBuilder, private chartservice: ChartService, private router: Router) { + this.showChartList(); + } + + ngOnInit(): void { + } + + chartType = CHARTTYPE; + constantGather = constantGather; + + // Currently, "Add a new chart" is not supported + onOpenModal() { + this.openModal = true; + } + // resetModal() { + // this.newchartForm.reset(); + // this.openModal = false; + // } + + chartlist: any = []; + errorMessage = "Service Error!" + isShowChartFailed: boolean = false; + isPageLoading: boolean = true; + //showChartList is to get the chart list + showChartList() { + this.isPageLoading = true; + this.isShowChartFailed = false; + this.chartservice.getChartList() + .subscribe((data: any) => { + this.chartlist = data.data; + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isShowChartFailed = true + this.isPageLoading = false + }); + } + + //refresh is for refresh button + refresh() { + this.showChartList(); + } + + +} diff --git a/frontend/src/app/view/content.component.html b/frontend/src/app/view/content.component.html new file mode 100644 index 00000000..a3df2694 --- /dev/null +++ b/frontend/src/app/view/content.component.html @@ -0,0 +1,11 @@ +
+ +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/content.component.scss b/frontend/src/app/view/content.component.scss new file mode 100644 index 00000000..4012b20f --- /dev/null +++ b/frontend/src/app/view/content.component.scss @@ -0,0 +1,5 @@ +.route { + flex: 1; + height: 100%; + overflow: auto; +} \ No newline at end of file diff --git a/frontend/src/app/view/content.component.spec.ts b/frontend/src/app/view/content.component.spec.ts new file mode 100644 index 00000000..3d2ac94c --- /dev/null +++ b/frontend/src/app/view/content.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContentComponent } from './content.component'; + +describe('ContentComponent', () => { + let component: ContentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ContentComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/content.component.ts b/frontend/src/app/view/content.component.ts new file mode 100644 index 00000000..3c0be06f --- /dev/null +++ b/frontend/src/app/view/content.component.ts @@ -0,0 +1,27 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router' +@Component({ + selector: 'app-content', + templateUrl: './content.component.html', + styleUrls: ['./content.component.scss'] +}) +export class ContentComponent implements OnInit { + + constructor(private route:Router) { } + navHide = '' + ngOnInit(): void { + this.navHide = this.route.url + } + +} diff --git a/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.html b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.html new file mode 100644 index 00000000..ce76c05c --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.html @@ -0,0 +1,133 @@ +
+ <<{{'CommonlyUse.back'| translate}} +

{{'EndpointDetail.name'| translate}}

+ + + {{errorMessage}} + + +
+
+ + + + +
+
+ + + + + +
+
    +
  • + {{'CommonlyUse.name'| translate}}: + {{endpointDetail.name}} +
  • +
  • + {{'CommonlyUse.description'| translate}}: + {{endpointDetail.description}} +
  • +
  • + {{'CommonlyUse.type'| translate}}: + {{endpointDetail.type}} +
  • +
  • + {{'CommonlyUse.version'| translate}}: + {{endpointDetail.kubefate_version}} +
  • +
  • + {{'CommonlyUse.creationTime'| translate}}: + {{endpointDetail.created_at | dateFormat}} +
  • +
  • + {{'EndpointDetail.host'| translate}}: + {{endpointDetail.kubefate_host}} +
  • +
  • + {{'CommonlyUse.ip'| translate}}: + {{endpointDetail.kubefate_address}} +
  • +
  • + {{'EndpointDetail.infraProviderName'| translate}}: + {{endpointDetail.infra_provider_name}} +
  • +
  • + {{'CommonlyUse.status'| translate}}: + {{constantGather('endpointstatus', + endpointDetail.status).name | translate}} +
  • +
+
+
+
    +
  • +

    {{'EndpointDetail.YAML'| translate}}:

    + +
  • +
+
+
+
+
+ +
+
+
+ + + + + + + +
+
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.scss b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.scss new file mode 100644 index 00000000..7ae028db --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.scss @@ -0,0 +1,73 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } + } + .pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255,255,255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; + } + +.card { + &.show { + visibility: hidden; + } + width: 100%; + min-width: 900px; + .list { + width: 100%; + input { + width: 100%; + } + select{ + width: 150px; + } + li { + &.codemirror { + width: 800px; + height: 800px; + } + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 200px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 700px; + height: 600px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; + } +clr-alert { + width: 50%; +} +.card1 { + margin-top: 6px; +} +.tog{ + display: flex; + margin-right: 12px; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.spec.ts b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.spec.ts new file mode 100644 index 00000000..330d299c --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointDetailComponent } from './endpoint-detail.component'; + +describe('EndpointDetailComponent', () => { + let component: EndpointDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EndpointDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EndpointDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.ts b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.ts new file mode 100644 index 00000000..01cfa5a2 --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-detail/endpoint-detail.component.ts @@ -0,0 +1,147 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service'; +import { ENDPOINTSTATUS, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-endpoint-detail', + templateUrl: './endpoint-detail.component.html', + styleUrls: ['./endpoint-detail.component.scss'] +}) +export class EndpointDetailComponent implements OnInit { + + constructor(private endpointservice: EndpointService, private router: Router, private route: ActivatedRoute) { } + + ngOnInit(): void { + this.showEndpointDetail(); + } + endpointStatus = ENDPOINTSTATUS; + constantGather = constantGather + //uuid of current endpoint + uuid = String(this.route.snapshot.paramMap.get('id')); + endpointDetail: any + errorMessage = "Service Error!" + code: any + overview = true + get isOverview() { + return this.overview + } + set isOverview(value) { + if (value) { + if (this.code) { + this.code.setValue(this.endpointDetail.kubefate_deployment_yaml) + this.overview = value + } else { + setTimeout(() => { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + this.code.setValue(this.endpointDetail ? this.endpointDetail.kubefate_deployment_yaml : '') + this.overview = value + }); + } + } else { + this.code = null + } + } + + isShowEndpointDetailFailed: boolean = false; + isPageLoading: boolean = true; + //showEndpointDetail is to get the endpoint detail + showEndpointDetail() { + this.isShowEndpointDetailFailed = false; + this.endpointservice.getEndpointDetail(this.uuid) + .subscribe((data: any) => { + const value = data.data.kubefate_deployment_yaml + if (this.code) { + this.code.setValue(value) + } else { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + this.code.setValue(value) + } + this.endpointDetail = data.data; + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isPageLoading = false + this.isShowEndpointDetailFailed = true + } + ); + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + this.uninstall = true; + } + uninstall: boolean = true; + //deleteEndpoint is to delete the endpoint + deleteEndpoint() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.endpointservice.deleteEndpoint(this.uuid, this.uninstall) + .subscribe(() => { + this.router.navigate(['/endpoint']); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + ischeckFailed = false + isLoading = false; + ischeckSuccess = false; + //check is to check the connection of endpoint + check() { + this.isLoading = true; + this.endpointservice.checkEndpoint(this.uuid) + .subscribe( + (data: any) => { + this.isLoading = false; + this.ischeckFailed = false; + this.ischeckSuccess = true; + }, + err => { + this.ischeckFailed = true + this.errorMessage = err.error.message; + this.isLoading = false; + this.ischeckSuccess = false; + }) + } +} diff --git a/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.html b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.html new file mode 100644 index 00000000..1a9cebfe --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.html @@ -0,0 +1,82 @@ +
+
+

{{'EndpointMg.name'|translate}}

+ + + {{errorMessage}} + + +
+ + + + + + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.version'|translate}} + {{'CommonlyUse.creationTime'| translate}} + {{'CommonlyUse.ip'|translate}} + {{'EndpointMg.infraName'|translate}} + {{'EndpointMg.infraUUID'|translate}} + {{'CommonlyUse.status'| translate}} + {{'CommonlyUse.action'| translate}} + + + {{endpoint.name}} + {{endpoint.description}} + {{endpoint.type}} + {{endpoint.kubefate_version}} + {{endpoint.created_at | dateFormat}} + {{endpoint.kubefate_address}} + {{endpoint.infra_provider_name}} + {{endpoint.infra_provider_uuid}} + + {{constantGather('endpointstatus', endpoint.status).name | + translate}} + + {{'CommonlyUse.delete'| translate}} + + + {{endpointlist ? endpointlist.length : 0}} item(s) + +
+ + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.scss b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.scss new file mode 100644 index 00000000..2a022170 --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.scss @@ -0,0 +1,25 @@ +.card { + .list { + margin-top: -24px; + input { + width: 100%; + } + select{ + width: 150px; + } + } + .t2{ + width: 100%; + height: 60px; + } +} + +.refreshbtn { + float: right; + margin-right: 12px; + margin-top: 24px; +} +.tog{ + display: flex; + margin-right: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.spec.ts b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.spec.ts new file mode 100644 index 00000000..cd38021a --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointMgComponent } from './endpoint-mg.component'; + +describe('EndpointMgComponent', () => { + let component: EndpointMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EndpointMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EndpointMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.ts b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.ts new file mode 100644 index 00000000..1fd09249 --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-mg/endpoint-mg.component.ts @@ -0,0 +1,96 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service'; +import { ENDPOINTSTATUS, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-endpoint-mg', + templateUrl: './endpoint-mg.component.html', + styleUrls: ['./endpoint-mg.component.scss'] +}) +export class EndpointMgComponent implements OnInit { + + constructor(private endpointservice: EndpointService, private router: Router) { } + + ngOnInit(): void { + this.showEndpointList(); + } + + endpointStatus = ENDPOINTSTATUS; + constantGather = constantGather + + endpointlist: any = [] + errorMessage = "Service Error!" + isShowEndpointFailed: boolean = false + isPageLoading: boolean = true + //showEndpointList is to get showEndpointList + showEndpointList() { + this.isShowEndpointFailed = false; + this.isPageLoading = true; + this.endpointservice.getEndpointList() + .subscribe((data: any) => { + this.endpointlist = data.data; + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isShowEndpointFailed = true; + this.isPageLoading = false; + }); + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal(uuid: string) { + this.pendingEndpoint = uuid; + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + this.uninstall = true; + } + + pendingEndpoint: string = ""; + uninstall: boolean = true; + //deleteEndpoint is to delete the Endpoint + deleteEndpoint() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.endpointservice.deleteEndpoint(this.pendingEndpoint, this.uninstall) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + //refresh is for refresh button + refresh() { + this.showEndpointList(); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + +} diff --git a/frontend/src/app/view/endpoint/endpoint-model.ts b/frontend/src/app/view/endpoint/endpoint-model.ts new file mode 100644 index 00000000..2f533e2f --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-model.ts @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface EndpointType { + created_at: string, + description: string, + infra_provider_name: string, + infra_provider_uuid: string, + kubefate_address: string, + kubefate_host: string, + name: string, + status: number, + type: string, + uuid: string, + is_compatible: boolean + is_managed: boolean +} \ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.html b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.html new file mode 100644 index 00000000..c887bbeb --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.html @@ -0,0 +1,255 @@ +
+ <<{{'CommonlyUse.back'| translate}} +
+

{{'EndpointNew.newEndpoint'| translate}}

+
+ +
+ + + {{'EndpointNew.selectInfra'| translate}} + + + + + {{'EndpointNew.noIinfra'| translate}} {{'EndpointNew.clickHere'| translate}} + + +
{{'EndpointNew.currentInfra'| translate}}:    {{selectedInfra?.name}}
+ + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'|translate}} + + {{infra.name}} + {{infra.description}} + {{infra.type}} + {{infra.created_at|dateFormat}} + + +
+ +
+
+ + + + {{'EndpointNew.endpointConfiguration'| translate}} + + + + + + +
+ + + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'|translate}} + + {{endpoint.name}} + {{endpoint.description}} + {{endpoint.type}} + {{endpoint.created_at| dateFormat}} + + +
+ + + +
+
+ + + + {{'CommonlyUse.installation'| translate}} + {{'EndpointNew.addEndpoint' | translate}} + + + + + + + + {{form.get('install')?.get('name')?.errors?.emptyMessage || + form.get('install')?.get('name')?.errors?.message | translate}} + {{'CommonlyUse.few' | + translate}}{{form.get('infraname')?.get('name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + {{'CommonlyUse.many' | + translate}}{{form.get('infraname')?.get('name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + + + + + + + +
{{'EndpointNew.serviceConfiguration'| translate}}
+
    + + + + {{ 'validator.empty' | translate}} + + + + + {{ 'validator.empty' | translate}} + + + + + {{ 'validator.empty' | translate}} + +
+
+
{{'EndpointNew.registryConfiguration'| translate}}
+
    + + + + + + + +
      + + + + {{'InfraProvider.imageName'|translate}}:    + {{registryConfig}} + federatedai/myimage:latest + + + {{'validator.empty'| translate}} + +
    +
    +
+
    + + + + + + + +
      +
    • + + + + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + {{'validator.internet'| translate}} + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    +
+
+
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + {{message}} + + + + + {{'CommonlyUse.submitting' | translate}} ... + + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.scss b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.scss new file mode 100644 index 00000000..c394465e --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.scss @@ -0,0 +1,75 @@ +li span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; +} +.t2{ + width: 500px; + height: 100px; +} +.t3{ + width: 600px; + height: 600px; +} +.nextbtn { + display: inline-block; + width: 100%; + height: 100%; + border: 1px solid #0072a3; +} +.btnnext { + margin: 0; + padding: 0; +} +.yamlbtn { + display: flex; + float: left; + margin-bottom: 0; + margin-right: 100%; +} +.no-warp::ng-deep { + width: 224px; + label { + line-height: 24px; + } + flex-direction: row; + justify-content: space-between; + align-items: center; +} +.no-warp2 { + width: 350px; + input { + width: 180px; + } +} +.no-warp3 { + width: 280px +} +.no-warp4 { + width: 520px; + input { + width: 345px; + } +} +.no-warp5 { + width: 367px +} +.btn-group { + margin-top: 10px; +} +.loading { + margin-top: 10px; +} +.yaml { + font-weight: 700; + margin: 10px 0; +} +.yaml-warp::ng-deep { + .clr-form-control { + .clr-textarea-wrapper { + width: 600px; + } + } +} diff --git a/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.spec.ts b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.spec.ts new file mode 100644 index 00000000..41a03ee6 --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EndpointNewComponent } from './endpoint-new.component'; + +describe('EndpointNewComponent', () => { + let component: EndpointNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EndpointNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EndpointNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts new file mode 100644 index 00000000..9eb24c14 --- /dev/null +++ b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts @@ -0,0 +1,494 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service' +import { ValidatorGroup } from 'src/utils/validators'; +import { InfraService } from 'src/app/services/common/infra.service'; +import * as CodeMirror from 'codemirror' +import { EndpointType } from 'src/app/view/endpoint/endpoint-model' +import { InfraType } from 'src/app/view/infra/infra-model' + + +@Component({ + selector: 'app-endpoint-new', + templateUrl: './endpoint-new.component.html', + styleUrls: ['./endpoint-new.component.scss'] +}) +export class EndpointNewComponent implements OnInit { + form: FormGroup; + constructor(private formBuilder: FormBuilder, private endpointService: EndpointService, private infraservice: InfraService, private router: Router) { + //form is the form to create a new endpoint + this.form = this.formBuilder.group({ + infra: this.formBuilder.group({ + infra_object: [], + infra_name: [''] + }), + endpoint: this.formBuilder.group({ + endpoint_type: [], + }), + install: this.formBuilder.group( + ValidatorGroup([ + { + name: 'name', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'yaml', + type: [''] + }, + { + name: 'service_username', + type: [''], + value: 'admin' + }, + { + name: 'service_password', + type: [''], + value: 'admin' + }, + { + name: 'hostname', + type: [''], + value: 'kubefate.net' + }, + { + name: 'need_ingress', + type: [''] + }, + { + name: 'ingress_controller_service_mode', + type: [''], + value: [0] + }, + { + name: 'useRegistry', + type: [''], + value: false + }, + { + name: 'useRegistrySecret', + type: [''], + value: false + }, + { + name: 'registryConfig', + type: [''], + value: '' + }, + { + name: 'server_url', + type: ['internet'], + value: '' + }, + { + name: 'username', + type: [''], + value: '' + }, + { + name: 'password', + type: [''], + value: '' + }, + ]) + ) + }); + } + + ngOnInit(): void { + this.getInfraList() + } + //needAdd is true when there is an endpoint on the selected infra but not recorded in the lifecycle manager + needAdd: boolean = false; + scanList: EndpointType[] = [] + message = 'Service Error!' + type: 'success' | 'info' | 'warning' | 'danger' = 'danger' + questResultFalg = false + infra!: InfraType | null + //selectedInfra is the selected infra in the 'Step 1' + get selectedInfra() { + return this.infra + } + set selectedInfra(value) { + this.infra = value + if (value) { + this.onSelectInfra() + } + } + + endpoint!: EndpointType | null + //selectedEndpoint is selected endpoint in the 'Step 2' when there is scaned endpoint list. + get selectedEndpoint() { + return this.endpoint + } + set selectedEndpoint(value) { + this.endpoint = value + if (!this.endpoint?.is_managed && this.endpoint?.is_compatible) { + this.needAdd = true; + this.needInstall = false; + } else { + this.needAdd = false + this.needInstall = false; + } + } + + infralist = []; + noInfra = false; + //getInfraList is to get the infra list in the 'Step 1' + getInfraList() { + this.infraservice.getInfraList().subscribe( + data => { + this.infralist = data.data; + if (this.infralist.length === 0) { + this.noInfra = true; + } + }, + err => { + this.infralist = [] + this.type = 'danger' + if (err.error.message) this.message = err.error.message + this.questResultFalg = true + } + ) + } + + needInstall = false; + scanLoading = false; + //postEndpointScan is to post the request to scan the endpoint on the selected infra + postEndpointScan(type: any) { + if (this.infra) { + this.scanLoading = true; + this.endpointService.postEndpointScan(this.infra.uuid, this.form.get('endpoint')?.get('endpoint_type')?.value,).subscribe( + (data: any) => { + this.scanList = data.data + if (this.scanList === null || this.scanList.length === 0) { + this.scanList = [] + this.needInstall = true; + this.needAdd = false; + } else { + this.selectedEndpoint = this.scanList[0]; + } + this.scanLoading = false + }, + err => { + this.type = 'danger' + this.message = err.error.message; + this.questResultFalg = true + this.scanLoading = false + } + ) + } + } + + // initYAMLEditorByEvent is triggered when clicking the yaml textarea(if there is no highlight) to reinitialize the YAML editor + initYAMLEditorByEvent(event: any) { + if (event && event.target && event.target.value) { + this.isGenerateSubmit = true; + this.initCodeMirror(event.target.value) + } + } + + // initCodeMirror is to initialize Code Mirror YAML editor + initCodeMirror(yamlContent: any) { + const yamlHTML = document.getElementById('yaml') as any + this.codeMirror = window.CodeMirror.fromTextArea(yamlHTML, { + value: yamlContent, + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + }) + this.codeMirror.on('change', (cm: any) => { + this.codeMirror.save() + }) + this.hasYAMLTextAreaDOM = true + } + + hasYAMLTextAreaDOM = false + // onClickNext is triggered when clicking the 'Next' button before the step 'YAML' + onClickNext() { + this.hasYAMLTextAreaDOM = false; + // when the form is reset by the clarity stepper component and the step 'YAML' is collapsed, the DOM of step 'YAML' will be cleared. + // So we need to check if we need to initialized code mirror window in the next step (step 'YAML) + if (document.getElementById('yaml') !== null) { + this.hasYAMLTextAreaDOM = true; + } + } + + get editorModeHelperMessage() { + return this.codeMirror && !this.hasYAMLTextAreaDOM && this.form.controls['install'].get('yaml')?.value + } + + //onSelectInfra is to reset form when selection change + infraSelectOk: boolean = false; + onSelectInfra() { + if (this.selectedInfra) { + this.infraSelectOk = true; + this.form.get('infra')?.get('infra_name')?.setValue(this.selectedInfra.name); + this.form.get('infra')?.get('infra_object')?.setValue(this.selectedInfra); + this.formResetChild('endpoint'); + this.formResetChild('install'); + this.form.get('install')?.get('service_username')?.setValue('admin') + this.form.get('install')?.get('service_password')?.setValue('admin') + this.form.get('install')?.get('hostname')?.setValue('kubefate.net') + this.showInfraDetail(this.selectedInfra.uuid) + this.selectedEndpoint = null + this.needAdd = false; + this.needInstall = false; + this.scanList = []; + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + this.isCreateEndpointFailed = false; + this.isCreateEndpointSubmit = false; + this.scanLoading = false; + this.noInfra = false; + this.message = ""; + } + } + + infraConfigDetail: any; + isShowInfraDetailFailed: boolean = false; + hasRegistry = false; + hasRegistrySecret = false; + registrySecretConfig: any; + errorMessage = "" + //showInfraDetail is to get the infra detail to get the saved registry/registry secret configuration + showInfraDetail(uuid: string) { + this.isShowInfraDetailFailed = false; + this.infraservice.getInfraDetail(uuid) + .subscribe((data: any) => { + this.infraConfigDetail = data.data.kubernetes_provider_info; + this.hasRegistry = this.infraConfigDetail.registry_config_fate.use_registry + this.hasRegistrySecret = this.infraConfigDetail.registry_config_fate.use_registry_secret + this.registrySecretConfig = this.infraConfigDetail.registry_config_fate.registry_secret_config + this.form.get('install')?.get('useRegistrySecret')?.setValue(this.hasRegistrySecret); + this.form.get('install')?.get('useRegistry')?.setValue(this.hasRegistry); + this.change_use_registry() + this.change_registry_secret() + }, + err => { + this.errorMessage = err.error.message; + this.isShowInfraDetailFailed = true; + } + ); + } + + get useRegistrySecret() { + return this.form.controls['install'].get('useRegistrySecret')?.value; + } + get useRegistry() { + return this.form.controls['install'].get('useRegistry')?.value; + } + get registryConfig() { + return this.form.controls['install'].get('registryConfig')?.value; + } + //onChange_use_registry is triggered when the value of 'use_registry' is changed + onChange_use_registry(val: any) { + this.change_use_registry() + this.selectChange(val) + } + change_use_registry() { + if (this.useRegistry) { + this.form.get('install')?.get('registryConfig')?.setValue(this.infraConfigDetail.registry_config_fate.registry); + } else { + this.form.get('install')?.get('registryConfig')?.setValue("-"); + } + } + + //onChange_use_registry_secret is triggered when the value of 'use_registry_secret' is changed + onChange_use_registry_secret(val: any) { + this.change_registry_secret() + this.selectChange(val) + } + change_registry_secret() { + if (this.useRegistrySecret) { + this.form.get('install')?.get('server_url')?.setValue(this.registrySecretConfig.server_url); + this.form.get('install')?.get('username')?.setValue(this.registrySecretConfig.username); + this.form.get('install')?.get('password')?.setValue(this.registrySecretConfig.password); + } + else { + this.form.get('install')?.get('server_url')?.setValue("https://x"); + this.form.get('install')?.get('username')?.setValue("-"); + this.form.get('install')?.get('password')?.setValue("-"); + } + } + + //registry_disabled is to valid if the registry/registry secret configuration are invalid + get registry_disabled() { + var registry_secret_valid: any = true; + if (this.useRegistrySecret) { + registry_secret_valid = (this.form.controls['install'].get('username')?.value?.trim() != '') && (this.form.controls['install'].get('password')?.value?.trim() != '') && this.form.get('install.server_url')?.valid && (this.form.controls['install'].get('server_url')?.value?.trim() != '') + } else { + registry_secret_valid = true + } + var registry_valid = true; + if (this.useRegistry) { + registry_valid = this.form.controls['install'].get('registryConfig')?.value?.trim() != '' + } else { + registry_valid = true + } + return !(registry_secret_valid && registry_valid) + } + + //validURL is to validate URL is valid + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + //server_url_suggestion to return the suggested server url based on the provided registry + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registryConfig === "" || this.registryConfig === null || this.registryConfig === "-") { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registryConfig.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + //valid_server_url return if the the server url is valid or not + get valid_server_url() { + return this.form.get('install.server_url')?.valid && this.form.controls['install'].get('server_url')?.value?.trim() != '' + } + + isGenerateSubmit = false; + isGenerateFailed = false; + codeMirror: any + //generateYaml() is to get the endpoint installation yaml based on the configuration provided by user + generateYaml() { + this.isGenerateSubmit = true; + const yamlHTML = document.getElementById('yaml') as any + if (!this.needAdd) { + var service_username = this.form.controls['install'].get('service_username')?.value; + var service_password = this.form.controls['install'].get('service_password')?.value; + var hostname = this.form.controls['install'].get('hostname')?.value; + var registry_server_url = this.useRegistrySecret ? this.form.controls['install'].get('server_url')?.value?.trim() : ""; + var registry_username = this.useRegistrySecret ? this.form.controls['install'].get('username')?.value?.trim() : ""; + var registry_password = this.useRegistrySecret ? this.form.controls['install'].get('password')?.value?.trim() : ""; + this.endpointService.getKubefateYaml(service_username, service_password, hostname, this.useRegistry, this.registryConfig, this.useRegistrySecret, registry_server_url, registry_username, registry_password).subscribe( + data => { + this.form.get('install')?.get('yaml')?.setValue(data.data); + this.isGenerateFailed = false; + if (this.codeMirror && this.hasYAMLTextAreaDOM) { + this.codeMirror.setValue(data.data) + } else { + this.initCodeMirror(data.data) + } + }, + err => { + this.message = err.error.message; + this.isGenerateFailed = true; + } + ) + } + } + + //selectChange is to reset YAML value when the releted confiuration is changed + selectChange(val: any) { + this.form.get('install')?.get('yaml')?.setValue(null); + if (this.codeMirror) { + this.codeMirror.setValue('') + } + + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + } + + //formResetChild is to reset group form, child is the group name in the form + formResetChild(child: string) { + //need to reset twice due to the issues of Clarity Steppers + this.form.controls[child].reset(); + this.form.controls[child].reset(); + } + + //generateYamlDisabled is to disabled the generate Yaml buttom when requied config provided by user id not vaild + get generateYamlDisabled() { + return !(this.form.controls['install'].get('service_username')?.value && this.form.controls['install'].get('service_password')?.value && this.form.controls['install'].get('hostname')?.value) + } + + //onSeclectIngress is triggered when the checkbox of ingress controller is selected + onSeclectIngress() { + if (!this.form.get('install')?.get('need_ingress')?.value) { + this.form.get('install')?.get('ingress_controller_service_mode')?.setValue(0); + } + } + get needIngress() { + return this.form.get('install')?.get('need_ingress')?.value + } + + isCreateEndpointFailed = false; + isCreateEndpointSubmit = false; + //createEndpoint is to submit 'create endpoint' request + createEndpoint() { + this.isCreateEndpointFailed = false; + this.isCreateEndpointSubmit = true; + if (this.form.valid) { + const endpointConfig = { + description: this.form.get('install')?.get('description')?.value, + infra_provider_uuid: this.infra?.uuid, + install: this.needInstall, + kubefate_deployment_yaml: this.form.get('install')?.get('yaml')?.value, + name: this.form.get('install')?.get('name')?.value, + type: this.form.get('endpoint')?.get('endpoint_type')?.value, + ingress_controller_service_mode: Number(this.form.get('install')?.get('ingress_controller_service_mode')?.value), + } + // validate + if (this.needInstall && endpointConfig.kubefate_deployment_yaml === '') { + this.message = "Kubefate Deployment YAML is required."; + this.isCreateEndpointFailed = true; + } else if (endpointConfig.type != 'KubeFATE') { + this.message = "Invalid endpoint type"; + this.isCreateEndpointFailed = true; + } else { + this.endpointService.createEndpoint(endpointConfig).subscribe( + (data: any) => { + this.router.navigateByUrl('/endpoint') + }, + err => { + this.message = err.error.message; + this.isCreateEndpointFailed = true; + } + ) + } + } + } + //createDisabled is to valid all the input and returns if create buttom need to be disabled + get createDisabled() { + var mustHave = this.form.controls['install'].get('name')?.value + var notUseIngress = !this.form.get('install')?.get('need_ingress')?.value && (Number(this.form.get('install')?.get('ingress_controller_service_mode')?.value) === 0) + var useIngress = this.form.get('install')?.get('need_ingress')?.value && + ((Number(this.form.get('install')?.get('ingress_controller_service_mode')?.value) === 1) || ( + Number(this.form.get('install')?.get('ingress_controller_service_mode')?.value) === 2)) + return !(mustHave && (this.needAdd || (!this.generateYamlDisabled && this.form.controls['install'].get('yaml')?.value && (notUseIngress || useIngress)))) || (this.isCreateEndpointSubmit && !this.isCreateEndpointFailed) + } +} diff --git a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.html b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.html new file mode 100644 index 00000000..1d08ae32 --- /dev/null +++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.html @@ -0,0 +1,235 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'ClusterDetail.detail'|translate}}

+ +
+ +
+
+ + + + + +
+
+ + +
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{clusterDetail.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{clusterDetail.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{clusterDetail.created_at | dateFormat}} +
  • +
  • + UUID: + {{clusterDetail.uuid}} +
  • +
  • + {{'NewCluster.namespace'|translate}}: + {{clusterDetail.namespace}} +
  • +
  • + {{'ClusterDetail.partyId'|translate}}: + {{clusterDetail.party_id}} +
  • +
  • + {{'CommonlyUse.status'|translate}}: + {{constantGather('participantFATEstatus', + clusterDetail.status).name | translate}} +
  • +
  • + {{'ExchangeDetail.infraProviderName'|translate}}: + {{clusterDetail.infra_provider_name}}{{clusterDetail.infra_provider_name}} +
  • +
  • + {{'ExchangeDetail.endpointName'|translate}}: + {{clusterDetail.endpoint_name}}{{clusterDetail.endpoint_name}} +
  • +
  • + {{'ExchangeDetail.clusterUuid'|translate}}: + {{clusterDetail.cluster_uuid}} +
  • +
+
+
+
+
{{'ExchangeDetail.accessInfo'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.Port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + + + + Site Portal + + {{acces.name}} + + {{acces.host}} + {{acces.port}} + {{acces.service_type ? acces.service_type : 'N/A'}} + {{acces.fqdn ? acces.fqdn : 'N/A'}} + {{acces.tls}} + + +
+
+
+
+
{{'ClusterDetail.ingressInfo'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'ClusterDetail.addresses'|translate}} + TLS + + + + {{ingress.name}} + + + Site Portal + + {{ingress.hosts}} + {{ingress.addresses}} + {{ingress.tls}} + + +
+
+
+
+
{{'ExchangeDetail.certificateInformation'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'ExchangeDetail.bindingMode'|translate}} + {{'ExchangeDetail.commonName'|translate}} + UUID + + + {{'ClusterDetail.pulsarServerCertInfo'|translate}} + {{constantGather('bindType', clusterDetail.pulsar_server_cert_info?.binding_mode).name | + translate}} + {{clusterDetail.pulsar_server_cert_info?.common_name ? + clusterDetail.pulsar_server_cert_info?.common_name : 'N/A'}} + {{clusterDetail.pulsar_server_cert_info?.uuid ? + clusterDetail.pulsar_server_cert_info?.uuid : 'N/A'}} + + + {{'ClusterDetail.sitePortalServerCertInfo'|translate}} + {{constantGather('bindType', + clusterDetail.site_portal_server_cert_info?.binding_mode).name | + translate}} + {{clusterDetail.site_portal_server_cert_info?.common_name ? + clusterDetail.site_portal_server_cert_info?.common_name : 'N/A'}} + {{clusterDetail.site_portal_server_cert_info?.uuid ? + clusterDetail.site_portal_server_cert_info?.uuid : 'N/A'}} + + + {{'ClusterDetail.sitePortalClientCertInfo'|translate}} + {{constantGather('bindType', clusterDetail.pulsar_server_cert_info?.binding_mode).name | + translate}} + {{clusterDetail.site_portal_client_cert_info?.common_name ? + clusterDetail.site_portal_client_cert_info?.common_name : 'N/A'}} + {{clusterDetail.site_portal_client_cert_info?.uuid ? + clusterDetail.site_portal_client_cert_info?.uuid : 'N/A'}} + + +
+
+
+
{{'ExchangeDetail.deploymentYaml'|translate}}:
+
+
    +
  • + +
  • +
+
+
+
+
+
+ + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.scss b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.scss new file mode 100644 index 00000000..55b299d0 --- /dev/null +++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.scss @@ -0,0 +1,153 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } +} +.pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255,255,255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; +} +.card1 { + width: 100%; + min-width: 1200px; + ol { + h6 { + margin: 0px; + } + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + b { + display: inline-block; + width: 300px; + } + } + .clr-form-control { + margin-top: 5px; + .clr-textarea-wrapper { + width: 600px; + } + textarea { + width: 600px; + height: 300px; + } + } + } + &.ol { + li { + border-bottom: 1px solid #999; + padding-bottom: 10px; + } + } + &.ol-ul { + margin-left: 30px; + li { + border-bottom: none; + padding-bottom: 0px; + span { + font-size: 12px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li { + height: 800px; + width: 800px; + } + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + &.child { + font-size: 12px; + } + + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 250px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} +.card { + min-width: 1200px; + .alert { + width: 30%; + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + display: inline-block; + // width: 200px; + &.bind-mode { + width: 115px; + } + } + .clr-form-control { + margin-top: 5px; + textarea { + width: 600px; + height: 300px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + + } +} +.deployment-yaml { + color: #333; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.spec.ts b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.spec.ts new file mode 100644 index 00000000..c95b3b1d --- /dev/null +++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClusterDetailComponent } from './cluster-detail.component'; + +describe('ClusterDetailComponent', () => { + let component: ClusterDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ClusterDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ClusterDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts new file mode 100644 index 00000000..8658604f --- /dev/null +++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts @@ -0,0 +1,190 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router' +import { FedService } from 'src/app/services/federation-fate/fed.service' +import { constantGather } from 'src/utils/constant'; +import { ChartService } from 'src/app/services/common/chart.service'; + + +@Component({ + selector: 'app-cluster-detail', + templateUrl: './cluster-detail.component.html', + styleUrls: ['./cluster-detail.component.scss'] +}) +export class ClusterDetailComponent implements OnInit { + isShowDetailFailed = false + isPageLoading = true + errorMessage = '' + //uuid is the federation UUID + uuid = '' + cluster_uuid = '' + deleteType = 'cluster' + forceRemove = false + constantGather = constantGather + clusterDetail: any = {} + openDeleteModal = false + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + accessInfoList: { [key: string]: any }[] = [] + ingressInfoList: { [key: string]: any }[] = [] + constructor(private route: ActivatedRoute, private router: Router, private fedService: FedService, private chartservice: ChartService) { } + ngOnInit(): void { + this.getClusterDetail() + } + code: any + overview = true + get isOverview() { + return this.overview + } + set isOverview(value) { + if (value) { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + if (this.clusterDetail.deployment_yaml) { + this.code.setValue(this.clusterDetail.deployment_yaml) + } + } else { + this.code = null + } + this.overview = value + } + + isManagedCluster = true; + //getClusterDetail is to get the Cluster Detail + getClusterDetail() { + this.isShowDetailFailed=false + this.openDeleteModal = false + this.isDeleteSubmit = false + this.isDeleteFailed = false + this.isShowDetailFailed = false + this.isPageLoading = true + this.errorMessage = '' + this.uuid = '' + this.cluster_uuid = '' + this.accessInfoList = [] + this.ingressInfoList = [] + this.route.params.subscribe( + value => { + this.uuid = value.id + this.cluster_uuid = value.cluster_uuid + if (this.uuid && this.cluster_uuid) { + this.fedService.getClusterInfo(value.id, value.cluster_uuid).subscribe( + data => { + this.clusterDetail = data.data + this.isManagedCluster = this.clusterDetail?.is_managed + this.getChartDetail(data.data.chart_uuid) + const value = data.data.deployment_yaml + if (this.code) { + this.code.setValue(value) + } + for (const key in data.data.access_info) { + const obj: any = { + name: key, + } + const value = data.data.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.accessInfoList.push(obj) + } + for (const key in data.data.ingress_info) { + const obj: any = { + name: key, + } + const value = data.data.ingress_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.ingressInfoList.push(obj) + } + this.isPageLoading = false + + }, + err => { + this.isShowDetailFailed = true + this.isPageLoading = false + if (err.error.message) this.errorMessage = err.error.message + } + ) + } + } + ) + + } + + isChartContainsPortalservices = false; + isShowChartFailed = false; + //getChartDetail is to get the chart if 'isChartContainsPortalservices' + getChartDetail(uuid: string) { + this.chartservice.getChartDetail(uuid) + .subscribe((data: any) => { + if (data.data) { + this.isChartContainsPortalservices = data.data.contain_portal_services; + } + this.isPageLoading = false; + this.isShowChartFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + } + + //refresh is for refresh button + refresh() { + this.getClusterDetail() + } + + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.openDeleteModal = true + } + + get accessInfo() { + return JSON.stringify(this.clusterDetail.access_info) === '{}' + } + + //toSitePortal is to open the SitePortal if the service is successfully deployed + toSitePortal(item: any) { + window.open( + `https://${item.host}:${item.port}`, '_blank' + ) + } + + //confirmDelete to submit the request of 'Delete cluster' + confirmDelete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.fedService.deleteParticipant(this.uuid, this.deleteType, this.cluster_uuid, this.forceRemove) + .subscribe(() => { + this.router.navigate(['/federation/fate', this.uuid]); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } +} diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.html b/frontend/src/app/view/federation/cluster-new/cluster-new.component.html new file mode 100644 index 00000000..592ece14 --- /dev/null +++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.html @@ -0,0 +1,441 @@ +
+ <<{{'CommonlyUse.back'| translate}} +
+

{{'NewCluster.name'| translate}}

+
+
+ + + {{'NewCluster.basicInformation'| translate}} + + + + + + + + + + + + + + + + + {{'validator.empty'| translate}} + + + + + + + + + + +
+ + + {{'NewCluster.externalClusterAccessInfo'|translate}} + + + + + + {{'validator.number'| translate}} + + +
+
{{'NewCluster.pulsar'|translate}}
+
    + + + + + {{form.get('external')?.get('pulsarHost')?.errors?.message | translate}} + + + + + + {{form.get('external')?.get('pulsarPort')?.errors?.message | translate}} + +
+
+
{{'NewCluster.nginx'|translate}}:
+
    + + + + + {{form.get('external')?.get('nginxHost')?.errors?.message | translate}} + + + + + + {{form.get('external')?.get('nginxPort')?.errors?.message | translate}} + +
+
+
+ + + {{errorMessage}} + + + + {{'CommonlyUse.submitting' | translate}} ... + + +
+
+
+ + +
+ + + {{'NewCluster.selectEndpoint'| translate}} + + + + + {{errorMessage}} + + +
{{'EndpointNew.currentSelection'|translate}}:    {{selectedEndpoint?.name}}
+ + + {{'NewCluster.noEndpoint'| translate}}{{'EndpointNew.clickHere'| translate}} + + + + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'| translate}} + {{'EndpointMg.infraName'|translate}} + {{'EndpointMg.endpointURL'|translate}} + {{'CommonlyUse.status'| translate}} + + {{endpoint?.name}} + {{endpoint?.description}} + {{endpoint?.type}} + {{endpoint?.created_at | date : "medium"}} + {{endpoint?.infra_provider_name}} + {{endpoint?.kubefate_host}} + {{constantGather('endpointstatus', endpoint?.status).name | translate}} + + + +
+
+ + + + {{'NewCluster.selectChart'| translate}} + + + + + {{errorMessage}} + + + + + + + + + + + + + {{'NewCluster.setPartyID'| translate}} + + + + + + {{'validator.number'| translate}} + + + + + + + + + {{errorMessage}} + + + + + + + + + {{'NewCluster.setNamespace'|translate}} + + + + + + + + + + + + + {{'NewCluster.selectCertificates'|translate}} + + + + + + + + + + + + +
+
1. {{'NewCluster.pulsarServerCertificate'|translate}}:
+ + + + + + + + + + + +
+
+
2. {{'NewCluster.certificate'|translate}}:
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + + {{'ExchangeNew.chooseServiceType'|translate}} + + + + + + + + + + + + + + + + + + + + {{'NewCluster.volumeConfig'|translate}} + + +
    + + + + + + + +
      +
    • + + + + {{ form.controls['persistence'].get('storageClassName')?.errors?.emptyMessage || + form.controls['persistence'].get('storageClassName')?.errors?.message | translate}} + + +
    • +
    +
+
+ +
+ + + + {{'InfraProvider.configuration' | translate}} + + +
    + + + + + + + +
      + + + + {{'InfraProvider.imageName'|translate}}:    + {{registryConfig}} + federatedai/myimage:latest + + + {{'validator.empty'| translate}} + +
    +
    +
+
    + + + + + + + +
      +
    • + + + + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + {{'validator.internet'| translate}} + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    +
+
+ +
+ + + + {{'PSP.pspConfigTitle' | translate}} + + + + + + + + + + + + + + + + {{'NewCluster.checkYAML'|translate}} + + + +
+ + + + + + + + {{errorMessage}} + + +
+ + {{'CommonlyUse.submitting' | translate}} ... + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss b/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss new file mode 100644 index 00000000..a71961d8 --- /dev/null +++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss @@ -0,0 +1,65 @@ +.alert { + width: 30%; +} +li span:first-of-type { + font-size: 13px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; +} +.t2{ + width: 300px; + height: 100px; +} +.yamlbtn { + display: flex; + float: left; + margin-bottom: 0; + margin-right: 100%; +} +.t2{ + width: 500px; + height: 100px; +} +.t3{ + width: 500px; + height: 400px; +} +// mark changes +.no-warp::ng-deep { + label { + margin-right: 25px; + line-height: 24px; + } + input { + width:300px; + } + flex-direction: row; + align-items: center; +} + +.no-warp2::ng-deep { + input { + width:400px; + } +} +// mark changes +.yaml { + font-weight: 700; + margin: 10px 0; +} +.yaml-warp::ng-deep { + .clr-form-control { + .clr-textarea-wrapper { + width: 600px; + } + } + .clr-accordion-status { + display: flex; + } +} + +.externalPartyIDLabel { + margin-bottom: 0px; +} \ No newline at end of file diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.spec.ts b/frontend/src/app/view/federation/cluster-new/cluster-new.component.spec.ts new file mode 100644 index 00000000..81f8c468 --- /dev/null +++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClusterNewComponent } from './cluster-new.component'; + +describe('ClusterNewComponent', () => { + let component: ClusterNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ClusterNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ClusterNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts b/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts new file mode 100644 index 00000000..f31c011f --- /dev/null +++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts @@ -0,0 +1,713 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service'; +import { FedService } from 'src/app/services/federation-fate/fed.service'; +import { ENDPOINTSTATUS, CHARTTYPE, constantGather } from 'src/utils/constant'; +import { ChartService } from 'src/app/services/common/chart.service'; +import { EndpointType } from 'src/app/view/endpoint/endpoint-model' +import { ValidatorGroup } from 'src/utils/validators' +import { InfraService } from 'src/app/services/common/infra.service'; + +@Component({ + selector: 'app-cluster-new', + templateUrl: './cluster-new.component.html', + styleUrls: ['./cluster-new.component.scss'] +}) +export class ClusterNewComponent implements OnInit { + form: FormGroup; + constructor(private formBuilder: FormBuilder, private fedservice: FedService, private router: Router, private route: ActivatedRoute, private endpointService: EndpointService, private chartservice: ChartService, private infraservice: InfraService) { + //form contains configuration to create a new cluster + this.form = this.formBuilder.group({ + info: this.formBuilder.group({ + name: [''], + description: [''], + clusterType: [''], + }), + external: this.formBuilder.group( + ValidatorGroup([ + { + name: 'party_id', + type: ['number'], + value: null + }, + { + name: 'pulsarHost', + type: ['ip'], + value: "" + }, + { + name: 'pulsarPort', + type: ['number'], + value: 0 + }, + { + name: 'nginxHost', + type: ['ip'], + value: "" + }, + { + name: 'nginxPort', + type: ['number'], + value: 0 + } + ]) + ), + endpoint: this.formBuilder.group({ + endpoint_uuid: [''] + }), + chart: this.formBuilder.group({ + chart_uuid: [''], + }), + namespace: this.formBuilder.group({ + namespace: [''], + }), + party: this.formBuilder.group( + ValidatorGroup([ + { + name: 'party_id', + type: ['number'], + value: null + } + ]) + ), + certificate: this.formBuilder.group({ + cert: ['skip'], + site_portal_client_cert_mode: [1], + site_portal_client_cert_uuid: [''], + site_portal_server_cert_mode: [1], + site_portal_server_cert_uuid: [''], + pulsar_server_cert_info: [1], + pulsar_server_cert_uuid: [''] + }), + serviceType: this.formBuilder.group({ + serviceType: [null], + }), + persistence: this.formBuilder.group( + ValidatorGroup([ + { + name: 'enablePersistence', + type: [''], + value: false + }, + { + name: 'storageClassName', + type: ['noSpace'], + value: "" + }, + ]) + ), + registry: this.formBuilder.group( + ValidatorGroup([ + { + name: 'useRegistry', + type: [''], + value: false + }, + { + name: 'useRegistrySecret', + type: [''], + value: false + }, + { + name: 'registryConfig', + type: [''], + value: '' + }, + { + name: 'server_url', + type: ['internet'], + value: '' + }, + { + name: 'username', + type: [''], + value: '' + }, + { + name: 'password', + type: [''], + value: '' + }, + ]) + ), + psp: this.formBuilder.group({ + enablePSP: [false], + }), + yaml: this.formBuilder.group({ + yaml: [''], + }) + }); + + this.showEndpointList(); + this.showChartList(); + } + + ngOnInit(): void { + } + + //support to create two type of exchange: create a new one or add an cluster + get clusterType() { + return this.form.get('info')?.get('clusterType')?.value + } + //isNewCluster returns true when user select to 'create a new one' + get isNewCluster() { + return this.form.get('info')?.get('clusterType')?.value === "new" + } + //isExternalCluster returns true when user select to 'Add an external one' + get isExternalCluster() { + return this.form.get('info')?.get('clusterType')?.value === "external" + } + + //submitExternalClusterDisable returns true when the input provided for adding an external Cluster is invaid + get submitExternalClusterDisable() { + return !this.form.controls['external'].valid || (this.isCreatedExternalSubmit && !this.isCreatedExternalFailed) + } + + isCreatedExternalSubmit = false; + isCreatedExternalFailed = false; + //createExternalCluster is to submit the request to 'create an external cluster' + createExternalCluster() { + this.isCreatedExternalSubmit = true; + this.isCreatedExternalFailed = false; + var externalCluster = { + name: this.form.controls['info'].get('name')?.value, + description: this.form.controls['info'].get('description')?.value, + federation_uuid: this.fed_uuid, + party_id: Number(this.form.get('external')?.get('party_id')?.value?.trim()), + pulsar_access_info: { + host: this.form.get('external')?.get('pulsarHost')?.value?.trim(), + port: Number(this.form.get('external')?.get('pulsarPort')?.value?.trim()), + fqdn: this.form.get('external')?.get('pulsarFQDN')?.value?.trim() + }, + nginx_access_info: { + host: this.form.get('external')?.get('nginxHost')?.value?.trim(), + port: Number(this.form.get('external')?.get('nginxPort')?.value?.trim()), + } + } + if (this.form.controls['external'].valid && this.form.controls['info'].valid) { + this.fedservice.createExternalCluster(this.fed_uuid, externalCluster) + .subscribe( + data => { + this.isCreatedExternalFailed = false; + this.router.navigateByUrl('/federation/fate/' + this.fed_uuid) + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedExternalFailed = true; + } + ); + } else { + this.errorMessage = "invalid input"; + this.isCreatedExternalFailed = true; + } + return + } + + endpointStatus = ENDPOINTSTATUS; + constantGather = constantGather + endpointlist: any = []; + errorMessage: any; + isShowEndpointFailed: boolean = false; + isPageLoading: boolean = true; + noEndpoint = false; + //showEndpointList is to get the ready endpoint list + showEndpointList() { + this.isShowEndpointFailed = false; + this.isPageLoading = true; + this.endpointlist = [] + this.endpointService.getEndpointList() + .subscribe((data: any) => { + if (data.data) { + for (var ep of data.data) { + if (ep?.status === 2) this.endpointlist?.push(ep) + } + if (this.endpointlist.length === 0) { + this.noEndpoint = true; + this.isPageLoading = false; + return + } + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowEndpointFailed = true; + this.isPageLoading = false; + }); + } + + fed_uuid = String(this.route.snapshot.paramMap.get('id')); + openInfraModal: boolean = false; + + endpoint!: EndpointType | null + //selectedEndpoint is the selected endpoint in the 'Step 2' + get selectedEndpoint() { + return this.endpoint + } + set selectedEndpoint(value) { + this.endpoint = value + if (value) { + this.onSelectEndpoint() + } + } + + use_cert: boolean = false; + skip_cert: boolean = false; + //setRadioDisplay is trigger when the selection of certificate's mode is changed + setRadioDisplay(val: any) { + if (val == "use") { + this.use_cert = true; + this.skip_cert = false; + } + if (val == "skip") { + this.use_cert = false; + this.skip_cert = true; + this.form.get('certificate')?.get('site_portal_client_cert_mode')?.setValue(1); + this.form.get('certificate')?.get('site_portal_server_cert_mode')?.setValue(1); + this.form.get('certificate')?.get('pulsar_server_cert_info')?.setValue(1); + } + } + + //cert_disabled is to validate the configuration of certificate section and disabled the 'Next' button if needed + get cert_disabled() { + var case1 = this.use_cert && this.isChartContainsPortalservices && (this.form.controls['certificate'].get('pulsar_server_cert_info')?.value === 1 || + this.form.controls['certificate'].get('site_portal_server_cert_mode')?.value === 1 + || this.form.controls['certificate'].get('site_portal_client_cert_mode')?.value === 1) + var case2 = this.use_cert && !this.isChartContainsPortalservices && this.form.controls['certificate'].get('pulsar_server_cert_info')?.value === 1 + return (case1 || case2) + } + + //service_type_disabled is to validate the configuration of Service type section and disabled the 'Next' button if needed + get service_type_disabled() { + return !this.form.controls['serviceType'].get('serviceType')?.valid + } + + //reset form when selection change + endpointSelectOk: boolean = false; + infraUUID: any = "" + onSelectEndpoint() { + if (this.selectedEndpoint) { + this.endpointSelectOk = true; + this.form.get('endpoint')?.get('endpoint_uuid')?.setValue(this.selectedEndpoint.uuid); + this.infraUUID = this.endpoint?.infra_provider_uuid + this.showInfraDetail(this.infraUUID) + } + } + + infraConfigDetail: any; + isShowInfraDetailFailed: boolean = false; + hasRegistry = false; + hasRegistrySecret = false; + registrySecretConfig: any; + //showInfraDetail is to get the registry/registry secret information that saved on the infra of the selected endpoint + showInfraDetail(uuid: string) { + this.isShowInfraDetailFailed = false; + this.infraservice.getInfraDetail(uuid) + .subscribe((data: any) => { + this.infraConfigDetail = data.data.kubernetes_provider_info; + this.hasRegistry = this.infraConfigDetail.registry_config_fate.use_registry + this.hasRegistrySecret = this.infraConfigDetail.registry_config_fate.use_registry_secret + this.registrySecretConfig = this.infraConfigDetail.registry_config_fate.registry_secret_config + this.form.get('registry')?.get('useRegistrySecret')?.setValue(this.hasRegistrySecret); + this.form.get('registry')?.get('useRegistry')?.setValue(this.hasRegistry); + this.change_use_registry() + this.change_registry_secret() + }, + err => { + this.errorMessage = err.error.message; + this.isShowInfraDetailFailed = true; + } + ); + } + + get useRegistrySecret() { + return this.form.controls['registry'].get('useRegistrySecret')?.value; + } + get useRegistry() { + return this.form.controls['registry'].get('useRegistry')?.value; + } + get registryConfig() { + return this.form.controls['registry'].get('registryConfig')?.value; + } + + //onChange_use_registry is triggered when the value of 'useRegistry' is changed + onChange_use_registry(val: any) { + this.change_use_registry() + this.selectChange(val) + } + change_use_registry() { + if (this.useRegistry) { + this.form.get('registry')?.get('registryConfig')?.setValue(this.infraConfigDetail.registry_config_fate.registry); + } else { + this.form.get('registry')?.get('registryConfig')?.setValue("-"); + } + } + + //onChange_use_registry_secre is triggered when the value of 'useRegistrySecret' is changed + onChange_use_registry_secret(val: any) { + this.change_registry_secret() + this.selectChange(val) + } + change_registry_secret() { + if (this.useRegistrySecret) { + this.form.get('registry')?.get('server_url')?.setValue(this.registrySecretConfig.server_url); + this.form.get('registry')?.get('username')?.setValue(this.registrySecretConfig.username); + this.form.get('registry')?.get('password')?.setValue(this.registrySecretConfig.password); + } + else { + this.form.get('registry')?.get('server_url')?.setValue("https://x"); + this.form.get('registry')?.get('username')?.setValue("-"); + this.form.get('registry')?.get('password')?.setValue("-"); + } + } + + //registry_disabled is to valid if the registry/registry secret configuration are invalid + get registry_disabled() { + var registry_secret_valid: any = true; + if (this.useRegistrySecret) { + registry_secret_valid = (this.form.controls['registry'].get('username')?.value?.trim() != '') && (this.form.controls['registry'].get('password')?.value?.trim() != '') && this.form.get('registry.server_url')?.valid && (this.form.controls['registry'].get('server_url')?.value?.trim() != '') + } else { + registry_secret_valid = true + } + var registry_valid = true; + if (this.useRegistry) { + registry_valid = this.form.controls['registry'].get('registryConfig')?.value?.trim() != '' + } else { + registry_valid = true + } + return !(registry_secret_valid && registry_valid) + } + + //persistence_disabled is to valid if the persistence configuration if invalid + get persistence_disabled() { + return !((this.form.controls['persistence'].get('enablePersistence')?.value && this.form.get('persistence.storageClassName')?.valid) || !this.form.controls['persistence'].get('enablePersistence')?.value) + } + + //enablePersistence returns if enable the Persistent volume + get enablePersistence() { + if (this.form.controls['persistence'].get('enablePersistence')?.value === null) { + this.form.get('persistence')?.get('storageClassName')?.setValue("-") + return false + } + return this.form.controls['persistence'].get('enablePersistence')?.value + } + + //onChange_enable_persistence is triggered when the value of 'enable_persistence' is changed + onChange_enable_persistence(val: any) { + this.selectChange(val) + this.change_enable_persistence() + } + change_enable_persistence() { + if (this.enablePersistence) { + this.form.get('persistence')?.get('storageClassName')?.setValue(""); + } else { + this.form.get('persistence')?.get('storageClassName')?.setValue("-"); + } + } + + //formResetChild is to reset group form, child is the group name in the form + formResetChild(child: string) { + //need to reset twice due to the issues of Clarity Steppers + this.form.controls[child].reset(); + this.form.controls[child].reset(); + if (child === 'yaml') { + this.codeMirror?.setValue('') + } + } + + partyId_valid = false; + isCheckSuccess = false; + isCheckFailed = false; + isLoading = false; + //checkPartyID is to check the confict of Party ID + checkPartyID() { + this.isCheckSuccess = false; + this.isCheckFailed = false; + this.isLoading = true; + if (this.form.controls['party'].get('party_id')?.value !== null) { + const party_id = Number(this.form.controls['party'].get('party_id')?.value); + this.fedservice.checkPartyID(this.fed_uuid, party_id).subscribe( + data => { + this.partyId_valid = true; + this.form.get('namespace')?.get('namespace')?.setValue('fate-' + party_id); + this.isLoading = false; + this.isCheckSuccess = true; + }, + err => { + this.errorMessage = err.error.message; + this.partyId_valid = false; + this.isLoading = false; + this.isCheckFailed = true; + } + ) + } else { + this.partyId_valid = false; + this.isLoading = false; + this.isCheckFailed = true; + this.errorMessage = "invalid party ID"; + } + } + + //editPartyID is to enable the form to edit and reset the value of yaml + editPartyID() { + this.partyId_valid = false; + this.isCheckSuccess = false; + this.isCheckFailed = false; + this.isLoading = false; + this.formResetChild('yaml'); + this.formResetChild('party'); + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + } + + //selectChange is to reset YAML value when the releted confiuration is changed + selectChange(val: any) { + this.formResetChild('yaml'); + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + } + + //onChartChange is triggered when the selection of chart is changed + isChartContainsPortalservices = false; + onChartChange(val: any) { + var chart_uuid = this.form.controls['chart'].get('chart_uuid')?.value; + this.chartservice.getChartDetail(chart_uuid) + .subscribe((data: any) => { + if (data.data) { + //isChartContainsPortalservices to decide the certificate we need when creating the exchange + this.isChartContainsPortalservices = data.data.contain_portal_services; + } + this.isPageLoading = false; + this.isShowChartFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + this.selectChange(val); + this.resetCert(); + } + + chartType = CHARTTYPE; + chartlist: any = []; + isShowChartFailed: boolean = false; + //showChartList is to get the chart list of deloying exchange + showChartList() { + this.chartlist = []; + this.isPageLoading = true; + this.isShowChartFailed = false; + this.chartservice.getChartList() + .subscribe((data: any) => { + if (data.data) { + for (let chart of data.data) { + if (chart.type === 2) { + this.chartlist.push(chart); + } + } + } + this.isPageLoading = false; + this.isShowChartFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + } + + //resetCert is triggered when the selection of chart is changed + resetCert() { + this.formResetChild('certificate'); + this.form.get('certificate')?.get('cert')?.setValue("skip"); + this.setRadioDisplay("skip"); + } + + //enablePSP returns if enable the pod secrurity policy + get enablePSP() { + return this.form.controls['psp'].get('enablePSP')?.value; + } + + isGenerateSubmit = false; + isGenerateFailed = false; + codeMirror: any + //generateYaml is to get the exchange initial yaml based on the configuration provided by user + generateClusterYaml() { + this.isGenerateSubmit = true; + var chart_id = this.form.controls['chart'].get('chart_uuid')?.value; + var namespace = this.form.controls['namespace'].get('namespace')?.value?.trim(); + var party_id = this.form.controls['party'].get('party_id')?.value; + var name = this.form.controls['info'].get('name')?.value?.trim(); + var service_type = Number(this.form.controls['serviceType'].get('serviceType')?.value); + var enable_persistence = this.form.controls['persistence'].get('enablePersistence')?.value; + if (enable_persistence === null) enable_persistence = false; + var storage_class = this.enablePersistence ? this.form.controls['persistence'].get('storageClassName')?.value?.trim() : ""; + if (namespace === '') namespace = 'fate-' + this.form.controls['party'].get('party_id')?.value; + this.fedservice.getClusterYaml(this.fed_uuid, chart_id, party_id, namespace, name, service_type, this.registryConfig, this.useRegistry, this.useRegistrySecret, enable_persistence, storage_class, this.enablePSP).subscribe( + data => { + this.form.get('yaml')?.get('yaml')?.setValue(data.data); + // if code mirror object and yaml DOM are existing, just set value to the code mirror object + // else initialize code mirror object and yaml editor window + if (this.codeMirror && this.hasYAMLTextAreaDOM) { + this.codeMirror.setValue(data.data) + } else { + this.initCodeMirror(data.data) + } + this.isGenerateFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isGenerateFailed = true; + } + ) + } + + //submitDisable returns if disabled the submit button of create a new cluster + get submitDisable() { + if (!this.form.controls['info'].valid || this.cert_disabled || this.service_type_disabled || !this.isGenerateSubmit || (this.isCreatedSubmit && !this.isCreatedFailed)) { + return true + } else { + return false + } + } + + // initYAMLEditorByEvent is triggered when clicking the yaml textarea(if there is no highlight) to reinitialize the YAML editor + initYAMLEditorByEvent(event: any) { + if (event && event.target && event.target.value) { + this.isGenerateSubmit = true; + this.initCodeMirror(event.target.value) + } + } + + // initCodeMirror is to initialize Code Mirror YAML editor + initCodeMirror(yamlContent: any) { + const yamlHTML = document.getElementById('yaml') as any + this.codeMirror = window.CodeMirror.fromTextArea(yamlHTML, { + value: yamlContent, + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + }) + this.codeMirror.on('change', (cm: any) => { + this.codeMirror.save() + }) + this.hasYAMLTextAreaDOM = true + } + + hasYAMLTextAreaDOM = false + // onClickNext is triggered when clicking the 'Next' button before the step 'YAML' + onClickNext() { + this.hasYAMLTextAreaDOM = false; + // when the form is reset by the clarity stepper component and the step 'YAML' is collapsed, the DOM of step 'YAML' will be cleared. + // So we need to check if we need to initialized code mirror window in the next step (step 'YAML) + if (document.getElementById('yaml') !== null) { + this.hasYAMLTextAreaDOM = true; + } + } + + get editorModeHelperMessage() { + return this.codeMirror && !this.hasYAMLTextAreaDOM && this.form.controls['yaml'].get('yaml')?.value + } + + isCreatedSubmit = false; + isCreatedFailed = false; + //createNewCluster is to submmit the request of 'create a new cluster' + createNewCluster() { + this.isCreatedFailed = false; + this.isCreatedSubmit = true; + const clusterInfo = { + chart_uuid: this.form.controls['chart'].get('chart_uuid')?.value, + deployment_yaml: this.codeMirror.getTextArea().value, + description: this.form.controls['info'].get('description')?.value, + endpoint_uuid: this.form.controls['endpoint'].get('endpoint_uuid')?.value, + federation_uuid: this.fed_uuid, + name: this.form.controls['info'].get('name')?.value, + namespace: this.form.controls['namespace'].get('namespace')?.value ? this.form.controls['namespace'].get('namespace')?.value : 'fate-' + this.form.controls['party'].get('party_id')?.value, + party_id: Number(this.form.controls['party'].get('party_id')?.value), + registry_config: { + use_registry: this.useRegistry, + use_registry_secret: this.useRegistrySecret, + registry: this.useRegistry ? this.registryConfig : "", + registry_secret_config: { + server_url: this.useRegistrySecret ? this.form.controls['registry'].get('server_url')?.value?.trim() : "", + username: this.useRegistrySecret ? this.form.controls['registry'].get('username')?.value?.trim() : "", + password: this.useRegistrySecret ? this.form.controls['registry'].get('password')?.value?.trim() : "", + } + }, + pulsar_server_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('pulsar_server_cert_info')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('pulsar_server_cert_uuid')?.value + }, + site_portal_client_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('site_portal_client_cert_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('site_portal_client_cert_uuid')?.value + }, + site_portal_server_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('site_portal_server_cert_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('site_portal_server_cert_uuid')?.value + } + } + this.fedservice.createCluster(this.fed_uuid, clusterInfo) + .subscribe( + data => { + this.isCreatedFailed = false; + this.router.navigateByUrl('/federation/fate/' + this.fed_uuid) + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + } + + //validURL is to validate URL is valid + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + //server_url_suggestion to return the suggested server url based on the provided registry + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registryConfig === "" || this.registryConfig === null || this.registryConfig === "-") { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registryConfig.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + //valid_server_url return if the the server url is valid or not + get valid_server_url() { + return this.form.get('registry.server_url')?.valid && this.form.controls['registry'].get('server_url')?.value?.trim() != '' + } + +} + diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html new file mode 100644 index 00000000..f61054ae --- /dev/null +++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html @@ -0,0 +1,203 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'ExchangeDetail.detail'|translate}}

+ +
+
+ + + + + +
+
+ + +
+
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{exchangeDetail?.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{exchangeDetail?.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{exchangeDetail?.created_at | dateFormat}} +
  • +
  • + UUID: + {{exchangeDetail?.uuid}} +
  • +
  • + {{'NewCluster.namespace'|translate}}: + {{exchangeDetail?.namespace}} +
  • +
  • + {{'CommonlyUse.status'|translate}}: + {{constantGather('participantFATEstatus', + exchangeDetail?.status).name | translate}} +
  • +
  • + {{'ExchangeDetail.infraProviderName'|translate}}: + {{exchangeDetail?.infra_provider_name}}{{exchangeDetail?.infra_provider_name}} +
  • +
  • + {{'ExchangeDetail.endpointName'|translate}}: + {{exchangeDetail?.endpoint_name}}{{exchangeDetail?.endpoint_name}} +
  • +
  • + {{'ExchangeDetail.clusterUuid'|translate}}: + {{exchangeDetail?.cluster_uuid}} +
  • +
+
+
+
+
{{'ExchangeDetail.accessInfo'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.Port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + {{acces.name}} + {{acces.host}} + {{acces.port}} + {{acces.service_type ? acces.service_type : 'N/A'}} + {{acces.fqdn ? acces.fqdn : 'N/A'}} + {{acces.tls}} + + +
+
+
+
+
{{'ExchangeDetail.certificateInformation'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'ExchangeDetail.bindingMode'|translate}} + {{'ExchangeDetail.commonName'|translate}} + UUID + + {{'ExchangeDetail.proxyServerCertInfo'|translate}} + {{constantGather('bindType', + exchangeDetail.proxy_server_cert_info.binding_mode).name | + translate}} + {{exchangeDetail?.proxy_server_cert_info?.common_name ? + exchangeDetail?.proxy_server_cert_info?.common_name : 'N/A'}} + {{exchangeDetail?.proxy_server_cert_info?.uuid ? + exchangeDetail?.proxy_server_cert_info?.uuid : 'N/A'}} + + + {{'ExchangeDetail.fmlManagerClientCertInfo'|translate}} + {{constantGather('bindType', + exchangeDetail?.fml_manager_client_cert_info?.binding_mode).name | + translate}} + {{exchangeDetail?.fml_manager_client_cert_info?.common_name ? + exchangeDetail?.fml_manager_client_cert_info?.common_name : 'N/A'}} + {{exchangeDetail?.fml_manager_client_cert_info?.uuid ? + exchangeDetail?.fml_manager_client_cert_info?.uuid : 'N/A'}} + + + {{'ExchangeDetail.fmlManagerServerCertInfo'|translate}} + {{constantGather('bindType', + exchangeDetail?.fml_manager_server_cert_info?.binding_mode).name | + translate}} + {{exchangeDetail?.fml_manager_server_cert_info?.common_name ? + exchangeDetail?.fml_manager_server_cert_info?.common_name : 'N/A'}} + {{exchangeDetail?.fml_manager_server_cert_info?.uuid ? + exchangeDetail?.fml_manager_server_cert_info?.uuid : 'N/A'}} + + +
+
+
+
{{'ExchangeDetail.deploymentYaml'|translate}}:
+
+
    +
  • + +
  • +
+
+
+
+
+
+ + + + + + + +
+
+
+ + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.scss b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.scss new file mode 100644 index 00000000..6d4ce54d --- /dev/null +++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.scss @@ -0,0 +1,166 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } +} +.card1 { + width: 100%; + min-width: 1200px; + ol { + h6 { + margin: 0px; + } + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + b { + display: inline-block; + width: 300px; + } + } + .clr-form-control { + margin-top: 5px; + .clr-textarea-wrapper { + width: 600px; + } + textarea { + width: 600px; + height: 300px; + } + } + } + &.ol { + li { + border-bottom: 1px solid #999; + padding-bottom: 10px; + } + } + &.ol-ul { + margin-left: 30px; + li { + border-bottom: none; + padding-bottom: 0px; + span { + font-size: 12px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + &.child { + li { + span{ + font-size: 12px; + } + } + } + + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} +.pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255, 255, 255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; +} +.card { + min-width: 1200px; + .alert { + width: 30%; + } + .list { + width: 100%; + &.first { + li::ng-deep { + span:first-of-type { + display: inline-block; + width: 250px; + } + } + } + li::ng-deep { + span:first-of-type { + display: inline-block; + // width: 250px; + &.bind-mode { + width: 115px; + } + } + .clr-form-control { + margin-top: 5px; + textarea { + width: 600px; + height: 300px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li { + height: 800px; + width: 800px; + } + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + + } +} + +.deployment-yaml { + color: #333; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.spec.ts b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.spec.ts new file mode 100644 index 00000000..a8d31056 --- /dev/null +++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExchangeDetailComponent } from './exchange-detail.component'; + +describe('ExchangeDetailComponent', () => { + let component: ExchangeDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExchangeDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExchangeDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts new file mode 100644 index 00000000..aab63197 --- /dev/null +++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts @@ -0,0 +1,167 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router' +import { FedService } from 'src/app/services/federation-fate/fed.service' +import { constantGather } from 'src/utils/constant'; +import { ChartService } from 'src/app/services/common/chart.service'; + +@Component({ + selector: 'app-exchange-detail', + templateUrl: './exchange-detail.component.html', + styleUrls: ['./exchange-detail.component.scss'] +}) +export class ExchangeDetailComponent implements OnInit { + isShowDetailFailed = false + isPageLoading = true + errorMessage = '' + //uuid is the federation UUID + uuid = '' + exchange_uuid = '' + constantGather = constantGather + exchangeDetail: any = {} + openDeleteModal = false + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + accessInfoList: { [key: string]: any }[] = [] + constructor(private route: ActivatedRoute, private router: Router, private fedService: FedService, private chartservice: ChartService) { } + ngOnInit(): void { + this.getExchangeDetail() + } + code: any + overview = true + get isOverview() { + return this.overview + } + set isOverview(value) { + if (value) { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + if (this.exchangeDetail.deployment_yaml) { + this.code.setValue(this.exchangeDetail.deployment_yaml) + } + } else { + this.code = null + } + this.overview = value + } + + isManagedExchange = true; + //getExchangeDetail is to get the Exchange Detail + getExchangeDetail() { + this.openDeleteModal = false + this.isDeleteSubmit = false + this.isDeleteFailed = false + this.isShowDetailFailed = false + this.isPageLoading = true + this.errorMessage = '' + this.uuid = '' + this.exchange_uuid = '' + this.route.params.subscribe( + value => { + this.uuid = value.id + this.exchange_uuid = value.exchange_uuid + if (this.uuid && this.exchange_uuid) { + this.fedService.getExchangeInfo(value.id, value.exchange_uuid).subscribe( + data => { + this.exchangeDetail = data.data + this.isManagedExchange = this.exchangeDetail?.is_managed + if (this.isManagedExchange) this.getChartDetail(data.data.chart_uuid) + const value = data.data.deployment_yaml + if (this.code) { + this.code.setValue(value) + } + for (const key in data.data.access_info) { + const obj: any = { + name: key, + } + const value = data.data.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.accessInfoList.push(obj) + } + this.isPageLoading = false + }, + err => { + this.isPageLoading = false + if (err.error.message) this.errorMessage = err.error.message + this.isShowDetailFailed = true + } + ) + } + } + ) + + } + + isChartContainsPortalservices = false; + isShowChartFailed = false; + //getChartDetail is to get the chart if 'isChartContainsPortalservices' + getChartDetail(uuid: string) { + this.chartservice.getChartDetail(uuid) + .subscribe((data: any) => { + if (data.data) { + //isChartContainsPortalservices to decide the certificate we need when creating the exchange + this.isChartContainsPortalservices = data.data.contain_portal_services; + } + this.isPageLoading = false; + this.isShowChartFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + } + + //refresh is for refresh button + refresh() { + this.accessInfoList = [] + this.getExchangeDetail() + } + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.openDeleteModal = true + } + + get accessInfo() { + return JSON.stringify(this.exchangeDetail.access_info) === '{}' + } + + deleteType = 'exchange' + forceRemove = false + //confirmDelete to submit the request of 'Delete exchange' + confirmDelete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.fedService.deleteParticipant(this.uuid, this.deleteType, this.exchange_uuid, this.forceRemove) + .subscribe(() => { + this.router.navigate(['/federation/fate', this.uuid]); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + + } +} diff --git a/frontend/src/app/view/federation/exchange-new/exchange-new.component.html b/frontend/src/app/view/federation/exchange-new/exchange-new.component.html new file mode 100644 index 00000000..d3990ea0 --- /dev/null +++ b/frontend/src/app/view/federation/exchange-new/exchange-new.component.html @@ -0,0 +1,377 @@ +
+ <<{{'CommonlyUse.back'| translate}} +
+

{{'ExchangeNew.name' | translate}}

+
+
+ + + {{'NewCluster.basicInformation'| translate}} + + + + + + + + + + + + + + + + + {{ 'validator.empty' | translate}} + + + + + + + + + + +
+ + + {{'ExchangeNew.externalExchangeAccessInfo'|translate}} + + +
1. {{'ExchangeNew.trafficServer'|translate}}
+
    + + + + + {{form.get('external')?.get('trafficServerHost')?.errors?.message | translate}} + + + + + + {{form.get('external')?.get('trafficServerPort')?.errors?.message | translate}} + +
+
+
2. {{'ExchangeNew.nginx'|translate}}:
+
    + + + + + {{form.get('external')?.get('nginxHost')?.errors?.message | translate}} + + + + + + {{form.get('external')?.get('nginxPort')?.errors?.message | translate}} + +
+
+
+ + + {{errorMessage}} + + + + {{'CommonlyUse.submitting' | translate}} ... + + +
+
+
+ + +
+ + + {{'NewCluster.selectEndpoint'| translate}} + + + + + {{errorMessage}} + + +
{{'EndpointNew.currentSelection'|translate}}:    {{selectedEndpoint?.name}}
+ + + {{'NewCluster.noEndpoint'| translate}}{{'EndpointNew.clickHere'| translate}} + + + + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'| translate}} + {{'EndpointMg.infraName'|translate}} + {{'EndpointMg.endpointURL'|translate}} + {{'CommonlyUse.status'| translate}} + + {{endpoint?.name}} + {{endpoint?.description}} + {{endpoint?.type}} + {{endpoint?.created_at | date : "medium"}} + {{endpoint?.infra_provider_name}} + {{endpoint?.kubefate_host}} + {{constantGather('endpointstatus', endpoint?.status).name | translate}} + + + +
+
+ + + + {{'NewCluster.selectChart'| translate}} + + + + + {{errorMessage}} + + + + + + + + + + + + + {{'NewCluster.setNamespace'|translate}} + + + + + + + + + + + + + {{'NewCluster.selectCertificates'|translate}} + + + + + + + + + + + + +
+
1. {{'ExchangeNew.proxyServerCertificate'|translate}}:
+ + + + + + + + + + + +
+
+
2. {{'ExchangeNew.certificate'|translate}}:
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + {{'ExchangeNew.chooseServiceType'|translate}} + + + + + + + + + + + + + + + + + + + + {{'InfraProvider.configuration' | translate}} + + +
    + + + + + + + +
      + + + + {{'InfraProvider.imageName'|translate}}:    + {{registryConfig}} + federatedai/myimage:latest + + + {{'validator.empty'| translate}} + +
    +
    +
+
    + + + + + + + +
      +
    • + + + + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + {{'validator.internet'| translate}} + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    +
+
+ +
+ + + + {{'PSP.pspConfigTitle' | translate}} + + + + + + + + + + + + + + + + {{'NewCluster.checkYAML'|translate}} + + + +
+ + + + + + + + {{errorMessage}} + + +
+ + {{'CommonlyUse.submitting' | translate}} ... + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/federation/exchange-new/exchange-new.component.scss b/frontend/src/app/view/federation/exchange-new/exchange-new.component.scss new file mode 100644 index 00000000..3daecefb --- /dev/null +++ b/frontend/src/app/view/federation/exchange-new/exchange-new.component.scss @@ -0,0 +1,47 @@ +.alert { + width: 30%; +} +li span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; +} +.t2{ + width: 300px; + height: 100px; +} +.yamlbtn { + display: flex; + float: left; + margin-bottom: 0; + margin-right: 100%; +} +.t2{ + width: 600px; + height: 100px; +} +.t3{ + width: 500px; + height: 400px; +} +.no-warp::ng-deep { + label { + margin-right: 15px; + line-height: 24px; + } + flex-direction: row; + align-items: center; +} +.yaml { + font-weight: 700; + margin: 10px 0; +} +.yaml-warp::ng-deep { + .clr-form-control { + .clr-textarea-wrapper { + width: 600px; + } + } +} diff --git a/frontend/src/app/view/federation/exchange-new/exchange-new.component.spec.ts b/frontend/src/app/view/federation/exchange-new/exchange-new.component.spec.ts new file mode 100644 index 00000000..2b081e4f --- /dev/null +++ b/frontend/src/app/view/federation/exchange-new/exchange-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExchangeNewComponent } from './exchange-new.component'; + +describe('ExchangeNewComponent', () => { + let component: ExchangeNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExchangeNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExchangeNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/exchange-new/exchange-new.component.ts b/frontend/src/app/view/federation/exchange-new/exchange-new.component.ts new file mode 100644 index 00000000..3a543c98 --- /dev/null +++ b/frontend/src/app/view/federation/exchange-new/exchange-new.component.ts @@ -0,0 +1,599 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service'; +import { FedService } from 'src/app/services/federation-fate/fed.service'; +import { ENDPOINTSTATUS, CHARTTYPE, constantGather } from 'src/utils/constant'; +import { ChartService } from 'src/app/services/common/chart.service'; +import { ValidatorGroup } from 'src/utils/validators' +import { InfraService } from 'src/app/services/common/infra.service'; +import { EndpointType } from 'src/app/view/endpoint/endpoint-model' + +@Component({ + selector: 'app-exchange-new', + templateUrl: './exchange-new.component.html', + styleUrls: ['./exchange-new.component.scss'] +}) +export class ExchangeNewComponent implements OnInit { + form: FormGroup; + + constructor(private formBuilder: FormBuilder, private fedservice: FedService, private router: Router, private route: ActivatedRoute, private endpointService: EndpointService, private chartservice: ChartService, private infraservice: InfraService) { + //form contains configuration to create a new exchange + this.form = this.formBuilder.group({ + info: this.formBuilder.group({ + name: [''], + description: [''], + exchangeType: [''], + }), + external: this.formBuilder.group( + ValidatorGroup([ + { + name: 'trafficServerHost', + type: ['ip'], + value: "" + }, + { + name: 'trafficServerPort', + type: ['number'], + value: 0 + }, + { + name: 'nginxHost', + type: ['ip'], + value: "" + }, + { + name: 'nginxPort', + type: ['number'], + value: 0 + } + ]) + ), + endpoint: this.formBuilder.group({ + endpoint_uuid: [''] + }), + chart: this.formBuilder.group({ + chart_uuid: [''], + }), + namespace: this.formBuilder.group({ + namespace: ['fate-exchange'], + }), + certificate: this.formBuilder.group({ + cert: ['skip'], + fml_manager_client_cert_mode: [1], + fml_manager_client_cert_uuid: [''], + fml_manager_server_cert_mode: [1], + fml_manager_server_cert_uuid: [''], + proxy_server_cert_mode: [1], + proxy_server_cert_uuid: [''] + }), + serviceType: this.formBuilder.group({ + serviceType: [null], + }), + registry: this.formBuilder.group( + ValidatorGroup([ + { + name: 'useRegistry', + type: [''], + value: false + }, + { + name: 'useRegistrySecret', + type: [''], + value: false + }, + { + name: 'registryConfig', + type: [''], + value: '' + }, + { + name: 'server_url', + type: ['internet'], + value: '' + }, + { + name: 'username', + type: [''], + value: '' + }, + { + name: 'password', + type: [''], + value: '' + }, + ]) + ), + psp: this.formBuilder.group({ + enablePSP: [false], + }), + yaml: this.formBuilder.group({ + yaml: [''], + }) + }); + this.showEndpointList(); + this.showChartList(); + } + ngOnInit(): void { + } + + fed_uuid = String(this.route.snapshot.paramMap.get('id')); + + //support to create two type of exchange: create a new one or add an external + get exchangeType() { + return this.form.get('info')?.get('exchangeType')?.value + } + //isNewExchange returns true when user select to 'create a new one' + get isNewExchange() { + return this.form.get('info')?.get('exchangeType')?.value === "new" + } + //isExternalExchange returns true when user select to 'Add an external one' + get isExternalExchange() { + return this.form.get('info')?.get('exchangeType')?.value === "external" + } + //submitExternalExchangeDisable returns true when the input provided for adding an external exchnage is invaid + get submitExternalExchangeDisable() { + return !this.form.controls['external'].valid || (this.isCreatedExternalSubmit && !this.isCreatedExternalFailed) + } + + isCreatedExternalSubmit = false; + isCreatedExternalFailed = false; + //createExternalExchange is to submit the request to 'create an external exchange' + createExternalExchange() { + this.isCreatedExternalSubmit = true; + this.isCreatedExternalFailed = false; + var externalExchange = { + name: this.form.controls['info'].get('name')?.value, + description: this.form.controls['info'].get('description')?.value, + federation_uuid: this.fed_uuid, + traffic_server_access_info: { + host: this.form.get('external')?.get('trafficServerHost')?.value?.trim(), + port: Number(this.form.get('external')?.get('trafficServerPort')?.value?.trim()), + }, + nginx_access_info: { + host: this.form.get('external')?.get('nginxHost')?.value?.trim(), + port: Number(this.form.get('external')?.get('nginxPort')?.value?.trim()), + } + } + if (this.form.controls['external'].valid && this.form.controls['info'].valid) { + this.fedservice.createExternalExchange(this.fed_uuid, externalExchange) + .subscribe( + data => { + this.isCreatedExternalFailed = false; + this.router.navigateByUrl('/federation/fate/' + this.fed_uuid) + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedExternalFailed = true; + } + ); + } else { + this.errorMessage = "invalid input"; + this.isCreatedExternalFailed = true; + } + return + } + + endpointStatus = ENDPOINTSTATUS; + constantGather = constantGather + endpointlist: any = []; + errorMessage: any; + isShowEndpointFailed: boolean = false; + isPageLoading: boolean = true; + noEndpoint = false; + //showEndpointList is to get the ready endpoint list + showEndpointList() { + this.isShowEndpointFailed = false; + this.isPageLoading = true; + this.endpointlist = [] + this.endpointService.getEndpointList() + .subscribe((data: any) => { + if (data.data) { + for (var ep of data.data) { + if (ep?.status === 2) this.endpointlist?.push(ep) + } + if (this.endpointlist.length === 0) { + this.noEndpoint = true; + this.isPageLoading = false; + return + } + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowEndpointFailed = true; + this.isPageLoading = false; + }); + } + + endpoint!: EndpointType | null + //selectedEndpoint is the selected endpoint in the 'Step 2' + get selectedEndpoint() { + return this.endpoint + } + set selectedEndpoint(value) { + this.endpoint = value + if (value) { + this.onSelectEndpoint() + } + } + + use_cert: boolean = false; + skip_cert: boolean = false; + //setRadioDisplay is triggered when the selection of certificate's mode is changed + setRadioDisplay(val: any) { + if (val == "use") { + this.use_cert = true; + this.skip_cert = false; + } + if (val == "skip") { + this.use_cert = false; + this.skip_cert = true; + this.form.get('certificate')?.get('fml_manager_client_cert_mode')?.setValue(1); + this.form.get('certificate')?.get('fml_manager_server_cert_mode')?.setValue(1); + this.form.get('certificate')?.get('proxy_server_cert_mode')?.setValue(1); + } + } + + //cert_disabled is to validate the configuration of certificate section and disabled the 'Next' button if needed + get cert_disabled() { + var case1 = this.use_cert && this.isChartContainsPortalservices && (this.form.controls['certificate'].get('proxy_server_cert_mode')?.value === 1 || + this.form.controls['certificate'].get('fml_manager_server_cert_mode')?.value === 1 + || this.form.controls['certificate'].get('fml_manager_client_cert_mode')?.value === 1) + var case2 = this.use_cert && !this.isChartContainsPortalservices && this.form.controls['certificate'].get('proxy_server_cert_mode')?.value === 1 + return (case1 || case2) + } + + //service_type_disabled is to validate the configuration of Service type section and disabled the 'Next' button if needed + get service_type_disabled() { + return !this.form.controls['serviceType'].get('serviceType')?.valid + } + + //reset form when selection change + endpointSelectOk: boolean = false; + infraUUID: any = "" + onSelectEndpoint() { + if (this.selectedEndpoint) { + this.endpointSelectOk = true; + this.form.get('endpoint')?.get('endpoint_uuid')?.setValue(this.selectedEndpoint.uuid); + this.infraUUID = this.endpoint?.infra_provider_uuid + this.showInfraDetail(this.infraUUID) + } + } + + infraConfigDetail: any; + isShowInfraDetailFailed: boolean = false; + hasRegistry = false; + hasRegistrySecret = false; + registrySecretConfig: any; + //showInfraDetail is to get the registry/registry secret information that saved on the infra of the selected endpoint + showInfraDetail(uuid: string) { + this.isShowInfraDetailFailed = false; + this.infraservice.getInfraDetail(uuid) + .subscribe((data: any) => { + this.infraConfigDetail = data.data.kubernetes_provider_info; + this.hasRegistry = this.infraConfigDetail.registry_config_fate.use_registry + this.hasRegistrySecret = this.infraConfigDetail.registry_config_fate.use_registry_secret + this.registrySecretConfig = this.infraConfigDetail.registry_config_fate.registry_secret_config + this.form.get('registry')?.get('useRegistrySecret')?.setValue(this.hasRegistrySecret); + this.form.get('registry')?.get('useRegistry')?.setValue(this.hasRegistry); + this.change_use_registry() + this.change_registry_secret() + }, + err => { + this.errorMessage = err.error.message; + this.isShowInfraDetailFailed = true; + } + ); + } + + get useRegistrySecret() { + return this.form.controls['registry'].get('useRegistrySecret')?.value; + } + get useRegistry() { + return this.form.controls['registry'].get('useRegistry')?.value; + } + get registryConfig() { + return this.form.controls['registry'].get('registryConfig')?.value; + } + + //onChange_use_registry is triggered when the value of 'useRegistry' is changed + onChange_use_registry(val: any) { + this.change_use_registry() + this.selectChange(val) + } + change_use_registry() { + if (this.useRegistry) { + this.form.get('registry')?.get('registryConfig')?.setValue(this.infraConfigDetail.registry_config_fate.registry); + } else { + this.form.get('registry')?.get('registryConfig')?.setValue("-"); + } + } + + //onChange_use_registry_secre is triggered when the value of 'useRegistrySecret' is changed + onChange_use_registry_secret(val: any) { + this.change_registry_secret() + this.selectChange(val) + } + change_registry_secret() { + if (this.useRegistrySecret) { + this.form.get('registry')?.get('server_url')?.setValue(this.registrySecretConfig.server_url); + this.form.get('registry')?.get('username')?.setValue(this.registrySecretConfig.username); + this.form.get('registry')?.get('password')?.setValue(this.registrySecretConfig.password); + } + else { + this.form.get('registry')?.get('server_url')?.setValue("https://x"); + this.form.get('registry')?.get('username')?.setValue("-"); + this.form.get('registry')?.get('password')?.setValue("-"); + } + } + + //registry_disabled is to valid if the registry/registry secret configuration are invalid + get registry_disabled() { + var registry_secret_valid: any = true; + if (this.useRegistrySecret) { + registry_secret_valid = (this.form.controls['registry'].get('username')?.value?.trim() != '') && (this.form.controls['registry'].get('password')?.value?.trim() != '') && this.form.get('registry.server_url')?.valid && (this.form.controls['registry'].get('server_url')?.value?.trim() != '') + } else { + registry_secret_valid = true + } + var registry_valid = true; + if (this.useRegistry) { + registry_valid = this.form.controls['registry'].get('registryConfig')?.value?.trim() != '' + } else { + registry_valid = true + } + return !(registry_secret_valid && registry_valid) + } + + //formResetChild is to reset group form, child is the group name in the form + formResetChild(child: string) { + //need to reset twice due to the issues of Clarity Steppers + this.form.controls[child].reset(); + this.form.controls[child].reset(); + if (child === 'yaml' && this.codeMirror) { + this.codeMirror.setValue('') + } + } + + chartType = CHARTTYPE; + chartlist: any = []; + isShowChartFailed: boolean = false; + //showChartList is to get the chart list of deloying exchange + showChartList() { + this.chartlist = []; + this.isPageLoading = true; + this.isShowChartFailed = false; + this.chartservice.getChartList() + .subscribe((data: any) => { + if (data.data) { + for (let chart of data.data) { + if (chart.type === 1) { + this.chartlist.push(chart); + } + } + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + } + + isChartContainsPortalservices = false; + //onChartChange is triggered when the selection of chart is changed + onChartChange(val: any) { + var chart_uuid = this.form.controls['chart'].get('chart_uuid')?.value; + this.chartservice.getChartDetail(chart_uuid) + .subscribe((data: any) => { + if (data.data) { + //isChartContainsPortalservices to decide the certificate we need when creating the exchange + this.isChartContainsPortalservices = data.data.contain_portal_services; + } + this.isPageLoading = false; + this.isShowChartFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + this.selectChange(val); + this.resetCert(); + } + + //enablePSP returns if enable the pod secrurity policy + get enablePSP() { + return this.form.controls['psp'].get('enablePSP')?.value; + } + + //resetCert is triggered when the selection of chart is changed + resetCert() { + this.formResetChild('certificate'); + this.form.get('certificate')?.get('cert')?.setValue("skip"); + this.setRadioDisplay("skip"); + } + + //selectChange is to reset YAML value when the releted confiuration is changed + selectChange(val: any) { + this.formResetChild('yaml'); + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + } + + isGenerateSubmit = false; + isGenerateFailed = false; + codeMirror: any + //generateYaml is to get the exchange initial yaml based on the configuration provided by user + generateYaml() { + this.isGenerateSubmit = true; + var chart_id = this.form.controls['chart'].get('chart_uuid')?.value; + var namespace = this.form.controls['namespace'].get('namespace')?.value; + var name = this.form.controls['info'].get('name')?.value; + var service_type = Number(this.form.controls['serviceType'].get('serviceType')?.value); + if (namespace === '') namespace = 'fate-exchange'; + this.fedservice.getExchangeYaml(chart_id, namespace, name, service_type, this.registryConfig, this.useRegistry, this.useRegistrySecret, this.enablePSP).subscribe( + data => { + this.form.get('yaml')?.get('yaml')?.setValue(data.data); + // if code mirror object and yaml DOM are existing, just set value to the code mirror object + // else initialize code mirror object and yaml editor window + if (this.codeMirror && this.hasYAMLTextAreaDOM) { + this.codeMirror.setValue(data.data) + } else { + this.initCodeMirror(data.data) + } + this.isGenerateFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isGenerateFailed = true; + } + ) + } + + // initYAMLEditorByEvent is triggered when clicking the yaml textarea(if there is no highlight) to reinitialize the YAML editor + initYAMLEditorByEvent(event: any) { + if (event && event.target && event.target.value) { + this.isGenerateSubmit = true; + this.initCodeMirror(event.target.value) + } + } + + // initCodeMirror is to initialize Code Mirror YAML editor + initCodeMirror(yamlContent: any) { + const yamlHTML = document.getElementById('yaml') as any + this.codeMirror = window.CodeMirror.fromTextArea(yamlHTML, { + value: yamlContent, + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + }) + this.codeMirror.on('change', (cm: any) => { + this.codeMirror.save() + }) + this.hasYAMLTextAreaDOM = true + } + + hasYAMLTextAreaDOM = false + // onClickNext is triggered when clicking the 'Next' button before the step 'YAML' + onClickNext() { + this.hasYAMLTextAreaDOM = false; + // when the form is reset by the clarity stepper component and the step 'YAML' is collapsed, the DOM of step 'YAML' will be cleared. + // So we need to check if we need to initialized code mirror window in the next step (step 'YAML) + if (document.getElementById('yaml') !== null) { + this.hasYAMLTextAreaDOM = true; + } + } + + get editorModeHelperMessage() { + return this.codeMirror && !this.hasYAMLTextAreaDOM && this.form.controls['yaml'].get('yaml')?.value + } + //submitDisable returns if disabled the submit button of create a new exchange + get submitDisable() { + if (!this.form.controls['info'].valid || this.cert_disabled || this.service_type_disabled || !this.isGenerateSubmit || (this.isCreatedSubmit && !this.isCreatedFailed)) { + return true + } else { + return false + } + } + isCreatedSubmit = false; + isCreatedFailed = false; + //createNewExchange is to submmit the request of 'create a new exchange' + createNewExchange() { + this.isCreatedFailed = false; + this.isCreatedSubmit = true; + const exchangeInfo = { + chart_uuid: this.form.controls['chart'].get('chart_uuid')?.value, + deployment_yaml: this.codeMirror.getTextArea().value, + description: this.form.controls['info'].get('description')?.value, + endpoint_uuid: this.form.controls['endpoint'].get('endpoint_uuid')?.value, + federation_uuid: this.fed_uuid, + registry_config: { + use_registry: this.useRegistry, + use_registry_secret: this.useRegistrySecret, + registry: this.useRegistry ? this.registryConfig : "", + registry_secret_config: { + server_url: this.useRegistrySecret ? this.form.controls['registry'].get('server_url')?.value?.trim() : "", + username: this.useRegistrySecret ? this.form.controls['registry'].get('username')?.value?.trim() : "", + password: this.useRegistrySecret ? this.form.controls['registry'].get('password')?.value?.trim() : "", + } + }, + fml_manager_client_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('fml_manager_client_cert_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('fml_manager_client_cert_uuid')?.value, + }, + fml_manager_server_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('fml_manager_server_cert_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('fml_manager_server_cert_uuid')?.value + }, + name: this.form.controls['info'].get('name')?.value, + namespace: this.form.controls['namespace'].get('namespace')?.value, + proxy_server_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('proxy_server_cert_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('proxy_server_cert_uuid')?.value + } + } + this.fedservice.createExchange(this.fed_uuid, exchangeInfo) + .subscribe( + data => { + this.isCreatedFailed = false; + this.router.navigateByUrl('/federation/fate/' + this.fed_uuid) + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + } + + //validURL is to validate URL is valid + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + //server_url_suggestion to return the suggested server url based on the provided registry + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registryConfig === "" || this.registryConfig === null || this.registryConfig === "-") { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registryConfig.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + //valid_server_url return if the the server url is valid or not + get valid_server_url() { + return this.form.get('registry.server_url')?.valid && this.form.controls['registry'].get('server_url')?.value?.trim() != '' + } + +} diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html new file mode 100644 index 00000000..d562c55b --- /dev/null +++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html @@ -0,0 +1,221 @@ +
+ <<{{'CommonlyUse.back'|translate}} +
+

{{'FederationDetail.name'|translate}}

+ + + + {{errorMessage}} + + +
+
+
+ + +
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{fedDetail?.name}} +
  • +
  • + {{'CommonlyUse.type'|translate}}: + {{fedDetail?.type}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{fedDetail?.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{fedDetail?.created_at | dateFormat}} +
  • +
  • + {{'Federation.domain'|translate}}: + {{fedDetail?.domain}} +
  • +
+
+
+

+ {{'FederationDetail.exchange'| translate}} +

+ + + {{'FederationDetail.noexchange'| translate}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{'FederationDetail.exchangeName'|translate}}{{'CommonlyUse.description'|translate}}{{'CommonlyUse.creationTime'|translate}}{{'FederationDetail.accessInfo'| translate}}{{'EndpointMg.name'|translate}}{{'InfraProvider.name'|translate}}{{'NewCluster.namespace'| translate}}{{'CommonlyUse.status'|translate}}{{'CommonlyUse.action'|translate}}
{{exchange.name}}{{exchange.description}}{{exchange.created_at | dateFormat}} + + + + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + {{exchange.name}} + {{exchange.host}} + {{exchange.port}} + {{exchange.service_type ? exchange.service_type : 'N/A'}} + {{exchange.fqdn ? exchange.fqdn : 'N/A'}} + {{exchange.tls}} + + + + + {{exchange.endpoint_name}}{{exchange.infra_provider_name}}{{exchange.namespace}}{{constantGather('participantFATEstatus', + exchange.status).name | translate}}{{'CommonlyUse.delete'|translate}}
+
+

+ {{'FederationDetail.cluster'|translate}} +

+ + + {{'FederationDetail.noactiveexchange'| translate}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{'FederationDetail.clusterName'|translate}}{{'CommonlyUse.description'|translate}}{{'CommonlyUse.creationTime'|translate}}{{'FederationDetail.accessInfo'| translate}}{{'EndpointMg.name'|translate}}{{'InfraProvider.name'|translate}}{{'NewCluster.namespace'| translate}}{{'FederationDetail.partyId'|translate}}{{'CommonlyUse.status'|translate}}{{'CommonlyUse.action'|translate}}
{{cluster.name}}{{cluster.description}}{{cluster.created_at | dateFormat}} + + + + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + + {{item.name}} + + {{item.host}} + {{item.port}} + {{item.service_type ? item.service_type : 'N/A'}} + {{item.fqdn ? item.fqdn : 'N/A'}} + {{item.tls}} + + + + + {{cluster.endpoint_name}}{{cluster.infra_provider_name}}{{cluster.namespace}}{{cluster.party_id}}{{constantGather('participantFATEstatus', + cluster.status).name | translate}}{{'CommonlyUse.delete'|translate}}
+
+
+ + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.scss b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.scss new file mode 100644 index 00000000..13a7793a --- /dev/null +++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.scss @@ -0,0 +1,100 @@ + +.card { + width: 700px; + .list { + input { + width: 100%; + } + select{ + width: 150px; + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.exchangealert { + width: 500px; +} +clr-toggle-container { + margin-top: 3px; +} +.sub { + margin-bottom: 12px; +} +table { + margin-top: 12px; + thead { + border-bottom: 1px solid #ccc; + tr { + background-color: #fafafa; + border-bottom: 1px solid #999; + th { + flex: 1; + border: none; + padding: 8px 0px 8px 12px; + min-width: 120px; + span { + display: flex; + flex: 1; + padding: 3px 0; + border-right: 1px solid #999; + } + &:last-of-type { + span { + border-right: none; + } + } + } + } + } + tbody { + tr{ + border-bottom: 1px solid #999; + td::ng-deep { + flex: 1; + border: none; + padding: 8px 12px; + min-width: 120px; + text-align: left; + .signpost { + .signpost-content-body { + padding: 20px 0px 0 0px; + } + } + } + + } + } +} +tr { + display: flex; + flex: 1; + align-items: center; + &.none { + display: block; + text-align: center; + padding-bottom: 20px; + } +} +.access_info { + width: 300px; + height: 150px; + border: none; +} +clr-alert { + margin-top: 12px; + margin-bottom: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.spec.ts b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.spec.ts new file mode 100644 index 00000000..5617a41c --- /dev/null +++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FedDetailFateComponent } from './fed-detail-fate.component'; + +describe('FedDetailFateComponent', () => { + let component: FedDetailFateComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FedDetailFateComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FedDetailFateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts new file mode 100644 index 00000000..f16afa4e --- /dev/null +++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts @@ -0,0 +1,190 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FedService } from 'src/app/services/federation-fate/fed.service'; +import { ParticipantFATEStatus, ParticipantFATEType, constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-fed-detail-fate', + templateUrl: './fed-detail-fate.component.html', + styleUrls: ['./fed-detail-fate.component.scss'] +}) +export class FedDetailFateComponent implements OnInit { + + constructor(private fedservice: FedService, private router: Router, private route: ActivatedRoute) { + this.showFedDetail(this.uuid); + this.showParticipantList(this.uuid) + } + + ngOnInit(): void { + } + + participantFATEstatus = ParticipantFATEType; + participantFATEtype = ParticipantFATEStatus; + constantGather = constantGather; + exchangeInfoList: any[] = [] + //uuid is the uuid of current federation + uuid = String(this.route.snapshot.paramMap.get('id')); + + fedDetail: any; + errorMessage = "Service Error!" + isShowFedDetailFailed: boolean = false; + isPageLoading: boolean = true; + //showFedDetail is to get the federation detail + showFedDetail(uuid: string) { + this.isPageLoading = true; + this.isShowFedDetailFailed = false; + this.fedservice.getFedDetail(uuid) + .subscribe((data: any) => { + this.fedDetail = data.data; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isPageLoading = false + this.isShowFedDetailFailed = true + } + ); + } + + participantList: any; + clusterlist: any[] = []; + exchange: any; + isShowParticipantListFailed: boolean = false; + //showParticipantList is to get the list exchange and clusters + showParticipantList(uuid: string) { + this.isPageLoading = true; + this.isShowParticipantListFailed = false; + this.fedservice.getFedParticipantList(uuid) + .subscribe((data: any) => { + this.participantList = data.data; + this.clusterlist = this.participantList.clusters || []; + this.exchange = this.participantList.exchange; + if (this.exchange) { + for (const key in this.exchange.access_info) { + const obj: any = { + name: key, + } + const value = this.exchange.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.exchangeInfoList.push(obj) + } + this.clusterlist.forEach(cluster => { + cluster.clusterList = [] + for (const key in cluster.access_info) { + const obj: any = { + name: key, + } + const value = cluster.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + cluster.clusterList.push(obj) + } + }); + } + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isPageLoading = false + this.isShowParticipantListFailed = true + } + ); + } + + //hasAccessInfo return if the participant contains access info + hasAccessInfo(object: any): boolean { + return JSON.stringify(object) != '{}' + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + deleteType: string = ''; + deleteUUID: string = ''; + forceRemove = false; + //openDeleteConfrimModal is to initial variables when open the modal of 'Delete Certificate' + openDeleteConfrimModal(type: string, item_uuid: string) { + this.deleteType = type; + this.deleteUUID = item_uuid; + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + this.forceRemove = false; + } + + //delete() is to delete federation or participant based on the buttom is clicked + delete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + //delete federation + if (this.deleteType === 'federation') { + this.fedservice.deleteFed(this.uuid) + .subscribe(() => { + this.router.navigate(['/federation']); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } else { + // delete particiapant + this.fedservice.deleteParticipant(this.uuid, this.deleteType, this.deleteUUID, this.forceRemove) + .subscribe(() => { + this.reloadCurrentRoute() + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + } + + //refresh is for refresh button + refresh() { + this.showFedDetail(this.uuid); + this.showParticipantList(this.uuid) + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //toDetail is redirect to the participant detail + toDetail(type: string, detailId: string, info: any) { + this.route.params.subscribe( + value => { + this.router.navigateByUrl(`federation/fate/${value.id}/${type}/detail/${detailId}`) + } + ) + } + + //createClusterDisabled is to disabled the 'new cluster' button when there is no active exchange in the current federation + get createClusterDisabled() { + if (this.exchange && this.exchange.status === 1) { + return false + } + return true + } +} diff --git a/frontend/src/app/view/federation/fed-mg/fed-mg.component.html b/frontend/src/app/view/federation/fed-mg/fed-mg.component.html new file mode 100644 index 00000000..74f620b6 --- /dev/null +++ b/frontend/src/app/view/federation/fed-mg/fed-mg.component.html @@ -0,0 +1,128 @@ +
+
+

{{'Federation.name'|translate}}

+ + + {{errorMessage}} + + +
+ + + + +
+ + {{'Federation.federationName'| translate}} + {{'CommonlyUse.description'|translate}} + {{'CommonlyUse.type'|translate}} + {{'CommonlyUse.creationTime'|translate}} + {{'CommonlyUse.action'|translate}} + + {{fed.name}} + + {{fed.description}} + {{fed.type}} + {{fed.created_at | dateFormat}} + {{'CommonlyUse.delete'|translate}} + + + {{federationList ? federationList.length : 0}} item(s) + +
+ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/federation/fed-mg/fed-mg.component.scss b/frontend/src/app/view/federation/fed-mg/fed-mg.component.scss new file mode 100644 index 00000000..b177cf95 --- /dev/null +++ b/frontend/src/app/view/federation/fed-mg/fed-mg.component.scss @@ -0,0 +1,127 @@ +.t2{ + width: 100%; + height: 100px; +} +.suffixHelper { + margin-top: 13px; + margin-left: -0.3px; +} + +input { + width: 97%; +} +select{ + width: 150px; +} +.refreshbtn { + float: right; + margin-right: 12px; + margin-top: 24px; +} +ol { + &.file-list { + margin-left: 110px; + } +} +.clr-input-contaner{ + position: relative; + align-items: flex-start; + box-sizing: border-box; + color: rgb(102, 102, 102); + display: flex; + flex-direction: row; + flex-wrap :wrap; + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: normal; + line-height: 24px; + margin-left: -12px; + margin-right: -12px; + margin-top: 24px; + text-size-adjust: 100%; + width: 534px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + label { + display: block; + color: var(--clr-forms-label-color, #454545); + font-size: .65rem; + font-weight: var(--clr-forms-label-font-weight, 600); + line-height: .9rem; + } + input { + border: 0; + border-bottom: 1px solid #b3b3b3; + width: 70%; + padding-right: 30px; + box-sizing: border-box; + &:focus-visible { + border: 0; + border-bottom: 1px solid #0072a3; + outline: none ; + } + &:focus { + border: 0; + border-bottom: 1px solid #0072a3; + outline: none; + } + &.error { + border-bottom: 1px solid #c21d00; + } + } + .file { + // width: 200px; + flex: 1; + position: relative; + top: -10px; + left: 15px; + } + .error-standard { + color: #c21d00; + top: 4px; + right: 25px; + position: absolute; + } + .valid-errot{ + margin-top: 0px; + font-size: 0.55rem; + color: #c21d00; + margin-left: 100px; + position: absolute; + bottom: -24px; + } +} +.clr-textrea::ng-deep { + width: 490px; + // .clr-control-container [ + // margin-left: 60px; + + // ] + .t3 { + margin-left: 104px; + border: 1px solid #b3b3b3; + padding: 0; + height: 150px; + overflow: auto; + border-radius: 0.15rem; + } +} +clr-modal { + .federation-type { + display: flex; + flex-direction: row; + .radio-title { + padding: 0 5px; + width: 89px; + } + } + form { + .margin-top-10 { + margin-top: 10px; + } + } +} + +.tooltip-icon { + float: right; +} \ No newline at end of file diff --git a/frontend/src/app/view/federation/fed-mg/fed-mg.component.spec.ts b/frontend/src/app/view/federation/fed-mg/fed-mg.component.spec.ts new file mode 100644 index 00000000..6a39ea71 --- /dev/null +++ b/frontend/src/app/view/federation/fed-mg/fed-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FedMgComponent } from './fed-mg.component'; + +describe('FedMgComponent', () => { + let component: FedMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FedMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FedMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/federation/fed-mg/fed-mg.component.ts b/frontend/src/app/view/federation/fed-mg/fed-mg.component.ts new file mode 100644 index 00000000..f4a2a09b --- /dev/null +++ b/frontend/src/app/view/federation/fed-mg/fed-mg.component.ts @@ -0,0 +1,236 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { FedService } from 'src/app/services/federation-fate/fed.service'; +import { OpenflService } from 'src/app/services/openfl/openfl.service' +import { ValidatorGroup } from 'src/utils/validators' +import { CreateOpenflComponent } from 'src/app/view/openfl/create-openfl-fed/create-openfl-fed.component' +import { AuthService } from 'src/app/services/common/auth.service'; +@Component({ + selector: 'app-fed-mg', + templateUrl: './fed-mg.component.html', + styleUrls: ['./fed-mg.component.scss'] +}) +export class FedMgComponent implements OnInit { + @ViewChild('create_openfl') create_openfl!: CreateOpenflComponent + federationList: any[] = []; + openModal: boolean = false; + disabled: boolean = true; + //default federation type is 'fate' + federationType = 'fate' + //fedInformationForm is form to create a new federation + fedInformationForm = this.fb.group( + ValidatorGroup([ + { + name: 'fedname', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'domain', + value: '', + type: ['fqdn'] + } + ]) + ) + + constructor(private fb: FormBuilder, private fedservice: FedService, private router: Router, private openflService: OpenflService, private authService: AuthService) { + this.getLCMServiceStatus(); + this.showFedList(); + } + + ngOnInit(): void { + } + fate: boolean = true; + openfl: boolean = false; + suffix: string = ""; + //setRadioDisplay is trigger when the selection of federation type is changed + setRadioDisplay(val: any) { + if (val == "fate") { + this.fate = true; + this.openfl = false; + } + if (val == "openfl") { + this.fate = false; + this.openfl = true; + } + } + //onOpenModal is to open the 'Create a new federation' modal + onOpenModal() { + this.federationType = "fate" + this.openModal = true; + } + //resetModal is for resetting the modal when close + resetModal() { + this.fedInformationForm.reset(); + this.openModal = false; + } + + errorMessage = "Service Error!" + isShowFedFailed: boolean = false; + Fedlist: any; + isPageLoading: boolean = true; + //showFedList is to show federation list + showFedList() { + this.isShowFedFailed = false; + this.isPageLoading = true; + this.fedservice.getFedList() + .subscribe((data: any) => { + this.federationList = data.data; + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isShowFedFailed = true + this.isPageLoading = false + }); + } + + isCreatedSubmit = false; + isCreatedFailed = false; + //submitDisable returns if disabled the submit button of create a new federation + get submitDisable() { + if (this.federationType === 'fate') { + if (this.fedInformationForm.valid) { + return false + } else { + return true + } + } else { + if (!this.create_openfl?.openflForm.get('customize')?.value) { + return !this.create_openfl?.customizeFalseValidate() + } else { + return !this.create_openfl?.customizeTrueValidate() + } + } + } + + //createNewFed is to submit the 'create federation' request + createNewFed() { + this.isCreatedFailed = false; + this.isCreatedSubmit = true; + // fate + if (this.federationType === 'fate') { + if (this.fedInformationForm.valid) { + const fedInfo = { + description: this.fedInformationForm.get('description')?.value, + domain: this.fedInformationForm.get('domain')?.value, + name: this.fedInformationForm.get('fedname')?.value + } + this.fedservice.createFed(fedInfo) + .subscribe( + data => { + this.isCreatedFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + } else { + this.errorMessage = "Invalid input"; + this.isCreatedFailed = true; + } + } else {// openfl + this.create_openfl.createNewOpenfl().subscribe( + data => { + this.isCreatedFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = "Invalid input"; + this.isCreatedFailed = true; + } + ) + } + + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + //openDeleteConfrimModal is to initial variables when open the modal of 'Delete federation' + openDeleteConfrimModal(uuid: string, fedType: string) { + this.pendingFed = uuid; + this.pendingFedType = fedType + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + } + + pendingFed: string = ""; + pendingFedType = '' + //deleteFed is to delete federation + deleteFed() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + if (this.pendingFedType === 'FATE') { + this.fedservice.deleteFed(this.pendingFed) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } else { + this.openflService.deleteOpenflFederation(this.pendingFed) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + } + + experimentEnabled = false; + isGetLCMStatusFailed = false; + //getLCMServiceStatus is to get the experiment is enabled or not + getLCMServiceStatus() { + this.isShowFedFailed = false; + this.isPageLoading = true; + this.authService.getLCMServiceStatus() + .subscribe((data: any) => { + this.experimentEnabled = data?.experiment_enabled; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isGetLCMStatusFailed = true + }); + } + + //refresh is for refresh button + refresh() { + this.showFedList(); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/infra', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} diff --git a/frontend/src/app/view/infra/infra-detail/infra-detail.component.html b/frontend/src/app/view/infra/infra-detail/infra-detail.component.html new file mode 100644 index 00000000..fc21e5e9 --- /dev/null +++ b/frontend/src/app/view/infra/infra-detail/infra-detail.component.html @@ -0,0 +1,301 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'InfraProvider.detail'|translate}}

+
+
+ + + + +
+ +
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{infraDetail.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{infraDetail.description}} +
  • +
  • + {{'CommonlyUse.type'|translate}}: + {{infraDetail.type}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{infraDetail.created_at | dateFormat}} +
  • +
+
+
+
+
{{'InfraProvider.information'|translate}}:
+
+
    +
  • + {{'InfraProvider.APIServer'|translate}}: + {{infraDetail.kubernetes_provider_info.api_server}} +
  • +
  • +

    {{'InfraProvider.kubeconfig'|translate}}:

    + +
  • +
+
+
+
+
+
{{'InfraProvider.configuration' | translate}}:
+
+
    +
  • + {{'InfraProvider.configureRegistry' | translate}}: + False +
  • +
  • + {{"InfraProvider.registry"|translate}}: + {{infraDetail.kubernetes_provider_info.registry_config_fate.registry}} +
  • +
  • + {{'InfraProvider.useRegistrySecret'|translate}}: + False +
  • +
    +
  • + {{'InfraProvider.serverURL'|translate}}: + {{infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.server_url}} +
  • +
  • + {{'InfraProvider.username'|translate}}: + {{infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.username}} +
  • +
  • + {{'InfraProvider.password'|translate}}: + ******* +
  • +
    +
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/infra/infra-detail/infra-detail.component.scss b/frontend/src/app/view/infra/infra-detail/infra-detail.component.scss new file mode 100644 index 00000000..ea042bec --- /dev/null +++ b/frontend/src/app/view/infra/infra-detail/infra-detail.component.scss @@ -0,0 +1,112 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } + } + .pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255, 255, 255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; + } + +.card1 { + width: 100%; + min-width: 1200px; + .list { + width: 100%; + input { + width: 100%; + } + select{ + width: 150px; + } + li { + &.codemirror::ng-deep { + .clr-textarea-wrapper { + width: 800px; + height: 800px; + } + } + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; + } + +.card3 { + width: 100%; + .list { + margin-top: -24px; + input { + width: 50%; + } + select{ + width: 130px; + } + } + .t2{ + width: 100%; + height: 60px; + } + .t3{ + width: 100%; + height: 300px; + } + .btnTest { + float: right; + margin-right: 10px; + } + .TestPassedIcon{ + float: right; + margin-right: 6px; + margin-top: 6px; + } +} + +.card4 { + width: 100%; + .list { + li { + margin-left: 12px; + } + clr-input-container { + input { + width: 50%; + } + } + clr-password-container { + width: 100%; + input { + width: 200%; + } + } + select{ + width: 130px; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/view/infra/infra-detail/infra-detail.component.spec.ts b/frontend/src/app/view/infra/infra-detail/infra-detail.component.spec.ts new file mode 100644 index 00000000..fda1e00d --- /dev/null +++ b/frontend/src/app/view/infra/infra-detail/infra-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InfraDetailComponent } from './infra-detail.component'; + +describe('InfraDetailComponent', () => { + let component: InfraDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ InfraDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InfraDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/infra/infra-detail/infra-detail.component.ts b/frontend/src/app/view/infra/infra-detail/infra-detail.component.ts new file mode 100644 index 00000000..5e0f1bcd --- /dev/null +++ b/frontend/src/app/view/infra/infra-detail/infra-detail.component.ts @@ -0,0 +1,370 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ValidatorGroup } from 'src/utils/validators'; +import { InfraService } from 'src/app/services/common/infra.service'; + +@Component({ + selector: 'app-infra-detail', + templateUrl: './infra-detail.component.html', + styleUrls: ['./infra-detail.component.scss'] +}) +export class InfraDetailComponent implements OnInit { + + //updateInfraForm is the form to update the infra information + updateInfraForm = this.fb.group( + ValidatorGroup([ + { + name: 'infraname', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'type', + type: ['require'] + }, + { + name: 'kubeconfig', + value: '', + type: ['require'] + }, + { + name: 'use_registry', + value: false, + type: [''] + }, + { + name: 'registry', + value: '', + type: [''] + }, + { + name: 'use_registry_secret', + value: false, + type: [''] + }, + { + name: 'server_url', + value: '', + type: ['internet'] + }, + { + name: 'username', + value: '', + type: [''] + }, + { + name: 'password', + value: '', + type: [''] + } + ]) + + ) + + constructor(private infraservice: InfraService, private router: Router, private route: ActivatedRoute, private fb: FormBuilder) { + } + + ngOnInit(): void { + this.showInfraDetail(this.uuid) + } + + uuid = String(this.route.snapshot.paramMap.get('id')); + infraDetail: any; + errorMessage = "Service Error!" + code: any + isShowInfraDetailFailed: boolean = false; + isPageLoading: boolean = true; + //showInfraDetail is to get the infra detailed information by UUID + async showInfraDetail(uuid: string) { + //first, get infra detail + await new Promise((re, rj) => { + this.isPageLoading = true; + this.infraservice.getInfraDetail(uuid) + .subscribe((data: any) => { + const yamlHTML = document.getElementById('yaml') as any + const value = data.data.kubernetes_provider_info.kubeconfig_content + if (!this.code) { + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + } + this.code.setValue(value) + this.infraDetail = data.data + this.isPageLoading = false + this.isShowInfraDetailFailed = false + re(this.isPageLoading) + re(this.isShowInfraDetailFailed) + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isPageLoading = false + this.isShowInfraDetailFailed = true + re(this.isPageLoading) + re(this.isShowInfraDetailFailed) + } + ); + }) + //second, test infra status + if (!this.isShowInfraDetailFailed && !this.isPageLoading) { + this.testConnection(true) + } + + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + } + + //deleteInfra is to submit 'delete infra' request + deleteInfra() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.infraservice.deleteInfra(this.uuid) + .subscribe(() => { + this.router.navigate(['/infra']); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + openModal: boolean = false; + //onOpenModal is to open the modal of 'Update the infra info' and initialize the form with current value + onOpenModal() { + this.updateInfraForm.controls['infraname'].setValue(this.infraDetail.name); + this.updateInfraForm.controls['type'].setValue(this.infraDetail.type); + this.updateInfraForm.controls['description'].setValue(this.infraDetail.description); + this.updateInfraForm.controls['kubeconfig'].setValue(this.infraDetail.kubernetes_provider_info.kubeconfig_content); + this.updateInfraForm.controls['use_registry_secret'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.use_registry_secret); + this.updateInfraForm.controls['server_url'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.server_url); + this.updateInfraForm.controls['username'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.username); + this.updateInfraForm.controls['password'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.password); + this.updateInfraForm.controls['registry'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry); + this.updateInfraForm.controls['use_registry'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.use_registry); + this.openModal = true; + this.isTestFailedModal = this.isTestFailed + this.testPassedModal = this.testPassed + } + //resetModal is to reset the modal when close the modal + resetModal() { + this.updateInfraForm.reset(); + this.openModal = false + this.errorMessageModal = "" + this.isUpdateFailed = false + this.isTestFailedModal = false + this.testPassedModal = false + } + + get use_registry_secret() { + return this.updateInfraForm.get('use_registry_secret')?.value + } + get use_registry() { + return this.updateInfraForm.get('use_registry')?.value + } + + //onChange_use_registry is triggered when the value of 'use_registry' is changed + onChange_use_registry() { + if (this.use_registry) this.updateInfraForm.controls['registry'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry); + if (!this.use_registry) this.updateInfraForm.controls['registry'].setValue(""); + } + + //onChange_use_registry_secret is triggered when the value of 'use_registry_secret' is changed + onChange_use_registry_secret() { + if (this.use_registry_secret) { + this.updateInfraForm.controls['server_url'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.server_url); + this.updateInfraForm.controls['username'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.username); + this.updateInfraForm.controls['password'].setValue(this.infraDetail.kubernetes_provider_info.registry_config_fate.registry_secret_config.password); + } + if (!this.use_registry_secret) { + this.updateInfraForm.controls['server_url'].setValue(""); + this.updateInfraForm.controls['username'].setValue(""); + this.updateInfraForm.controls['password'].setValue(""); + } + } + get registry() { + return this.updateInfraForm.get('registry')?.value + } + + // variable in infra detail page + isTestFailed = false + testPassed = false + isTestLoading = false + // variable in modal + isTestFailedModal = false + testPassedModal = true + isTestLoadingModal = false + errorMessageModal = "" + //testConnection is to test the k8s connection by using kubeconfig + testConnection(notUpdate: boolean) { + var kubeconfigContent = "" + if (notUpdate) { + kubeconfigContent = this.infraDetail.kubernetes_provider_info.kubeconfig_content + this.isTestLoading = true; + this.isTestFailed = false; + } else { + kubeconfigContent = this.updateInfraForm.get('kubeconfig')?.value + this.isTestLoadingModal = true; + this.isTestFailedModal = false; + } + this.infraservice.testK8sConnection(kubeconfigContent).subscribe(() => { + if (notUpdate) { + this.testPassed = true + this.isTestLoading = false + } else { + this.testPassedModal = true + this.isTestLoadingModal = false + } + }, + err => { + if (notUpdate) { + this.isTestFailed = true + this.testPassed = false + this.errorMessage = err.error.message + this.isTestLoading = false + } else { + this.isTestFailedModal = true + this.testPassedModal = false + this.errorMessageModal = err.error.message + this.isTestLoadingModal = false + } + }); + } + + //onKubeconfigChange is triggered when the input of kubeconfig is changed + onKubeconfigChange(val: any) { + this.testPassedModal = false; + this.isTestFailedModal = false; + } + + isUpdateFailed: boolean = false; + //updateInfra is to update the info of infra + updateInfra() { + this.isUpdateFailed = false; + var infraInfo = { + description: this.updateInfraForm.get('description')?.value, + kubernetes_provider_info: { + kubeconfig_content: this.updateInfraForm.get('kubeconfig')?.value, + registry_config_fate: { + use_registry: this.updateInfraForm.get('use_registry')?.value, + registry: this.use_registry ? this.updateInfraForm.get('registry')?.value?.trim() : "", + use_registry_secret: this.updateInfraForm.get('use_registry_secret')?.value, + registry_secret_config: { + server_url: this.use_registry_secret ? this.updateInfraForm.get('server_url')?.value?.trim() : "", + username: this.use_registry_secret ? this.updateInfraForm.get('username')?.value?.trim() : "", + password: this.use_registry_secret ? this.updateInfraForm.get('password')?.value?.trim() : "", + } + } + }, + name: this.updateInfraForm.get('infraname')?.value, + type: this.updateInfraForm.get('type')?.value, + } + this.infraservice.updateInfraProvider(infraInfo, this.uuid) + .subscribe( + data => { + this.isUpdateFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isUpdateFailed = true; + } + ); + } + + //submitDisable returns if disabled the submit button of create a new infra + get submitDisable() { + var registry_secret_valid = true; + if (this.use_registry_secret) { + registry_secret_valid = (this.updateInfraForm.get('username')?.value?.trim() != '') && (this.updateInfraForm.get('password')?.value?.trim() != '') && !(this.updateInfraForm.controls['server_url'].errors) + } else { + registry_secret_valid = true + } + var registry_valid = true; + if (this.use_registry) { + registry_valid = this.updateInfraForm.get('registry')?.value?.trim() != '' + } else { + registry_valid = true + } + var basics_valid = !this.updateInfraForm.controls['infraname'].errors && !this.updateInfraForm.controls['type'].errors + return !(this.testPassedModal && registry_secret_valid && registry_valid && basics_valid) + } + + //refresh is for refresh button + refresh() { + this.showInfraDetail(this.uuid); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //validURL is to validate URL is valid + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + //server_url_suggestion to return the suggested server url based on the provided registry + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registry === '' || this.registry === null) { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registry.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + //valid_server_url return if the the server url is valid or not + get valid_server_url() { + return !this.updateInfraForm.controls['server_url'].errors + } + +} diff --git a/frontend/src/app/view/infra/infra-model.ts b/frontend/src/app/view/infra/infra-model.ts new file mode 100644 index 00000000..d25743b2 --- /dev/null +++ b/frontend/src/app/view/infra/infra-model.ts @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface InfraType { + created_at: string, + description: string, + kubernetes_provider_info: any + name: string, + type: string, + uuid: string, +} + +export interface InfraResponse { + code: number; + message: string; + data: any; +} \ No newline at end of file diff --git a/frontend/src/app/view/infra/infra.component.html b/frontend/src/app/view/infra/infra.component.html new file mode 100644 index 00000000..6649f67c --- /dev/null +++ b/frontend/src/app/view/infra/infra.component.html @@ -0,0 +1,218 @@ +
+
+

{{'InfraProvider.name'|translate}}

+ + + {{errorMessage}} + + + + +
+ +
+
+ +
+ + + +
+ {{'CommonlyUse.name'|translate}} + {{'CommonlyUse.description'|translate}} + {{'CommonlyUse.type'|translate}} + {{'InfraProvider.server'|translate}} + UUID + {{'CommonlyUse.creationTime'|translate}} + + + {{infra.name}} + {{infra.description}} + {{infra.type}} + {{infra.kubernetes_provider_info.api_server}} + {{infra.uuid}} + {{infra.created_at | dateFormat}} + + + {{infralist ? infralist.length : 0}} item(s) +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/infra/infra.component.scss b/frontend/src/app/view/infra/infra.component.scss new file mode 100644 index 00000000..98a31e84 --- /dev/null +++ b/frontend/src/app/view/infra/infra.component.scss @@ -0,0 +1,36 @@ +.card { + .list { + &.card1 { + margin-top: -24px; + } + input { + width: 99%; + } + select{ + width: 130px; + } + } + .t2{ + width: 100%; + height: 60px; + } + .t3{ + width: 100%; + height: 200px; + } + .btnTest { + float: right; + margin-right: 10px; + } + .TestPassedIcon{ + float: right; + margin-right: 6px; + margin-top: 6px; + } +} + +.refreshbtn { + float: right; + margin-right: 12px; + margin-top: 12px; +} diff --git a/frontend/src/app/view/infra/infra.component.spec.ts b/frontend/src/app/view/infra/infra.component.spec.ts new file mode 100644 index 00000000..0e4ec337 --- /dev/null +++ b/frontend/src/app/view/infra/infra.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InfraComponent } from './infra.component'; + +describe('InfraComponent', () => { + let component: InfraComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ InfraComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InfraComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/infra/infra.component.ts b/frontend/src/app/view/infra/infra.component.ts new file mode 100644 index 00000000..f333cafb --- /dev/null +++ b/frontend/src/app/view/infra/infra.component.ts @@ -0,0 +1,305 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit} from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { InfraService } from 'src/app/services/common/infra.service'; +import { ValidatorGroup } from 'src/utils/validators' +import { InfraResponse } from './infra-model'; + +@Component({ + selector: 'app-infra', + templateUrl: './infra.component.html', + styleUrls: ['./infra.component.scss'] +}) +export class InfraComponent implements OnInit { + + selectedInfraList: any = []; + openModal: boolean = false; + //newInfraForm is the form to create an new infra + newInfraForm = this.fb.group( + ValidatorGroup([ + { + name: 'infraname', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'type', + type: ['require'] + }, + { + name: 'kubeconfig', + value: '', + type: ['require'] + }, + { + name: 'use_registry', + value: false, + type: [''] + }, + { + name: 'registry', + value: '', + type: ['require'] + }, + { + name: 'use_registry_secret', + value: false, + type: [''] + }, + { + name: 'server_url', + value: '', + type: ['internet'] + }, + { + name: 'username', + value: '', + type: [''] + }, + { + name: 'password', + value: '', + type: [''] + } + ]) + ) + constructor(private fb: FormBuilder, private infraservice: InfraService, private router: Router) { + } + + ngOnInit(): void { + this.showInfraList(); + this.newInfraForm.controls['use_registry'].setValue(false); + this.newInfraForm.controls['use_registry_secret'].setValue(false); + } + + errorMessage = "Service Error!" + isShowInfraFailed: boolean = false; + infralist: any; + isPageLoading: boolean = true; + //showInfraList is to get the infra list + showInfraList() { + this.isShowInfraFailed = false; + this.isPageLoading = true; + this.infraservice.getInfraList() + .subscribe((data: InfraResponse) => { + this.infralist = data.data; + this.isPageLoading = false; + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isShowInfraFailed = true + this.isPageLoading = false + }); + } + + //onOpenModal is to open the modal of 'Create an new infra' + onOpenModal() { + this.openModal = true; + } + //resetModal() is to close the modal of 'Create an new infra' and reset the modal of 'Create an new infra' + resetModal() { + this.newInfraForm.reset(); + this.openModal = false; + this.testPassed = false; + this.isTestFailed = false; + this.isCreatedFailed = false; + this.errorMessage = ""; + this.newInfraForm.controls['use_registry'].setValue(false); + this.newInfraForm.controls['use_registry_secret'].setValue(false); + } + + get use_registry_secret() { + return this.newInfraForm.get('use_registry_secret')?.value + } + get use_registry() { + return this.newInfraForm.get('use_registry')?.value + } + get registry() { + return this.newInfraForm.get('registry')?.value + } + //onChange_use_registry is triggered when the value of 'use_registry' is changed + onChange_use_registry() { + if (!this.use_registry) this.newInfraForm.controls['registry'].setValue(""); + } + //onChange_use_registry_secret is triggered when the value of 'use_registry_secret' is changed + onChange_use_registry_secret() { + if (!this.use_registry_secret) { + this.newInfraForm.controls['server_url'].setValue(""); + this.newInfraForm.controls['username'].setValue(""); + this.newInfraForm.controls['password'].setValue(""); + } + } + + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataList: string = ''; + //openConfirmModal() is to initialize variables when open the modal of "Delete Infra" + openConfirmModal() { + this.isDeleteFailed = false; + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + } + + //deleteInfra is to delete the selected infra(s) + deleteInfra() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + for (let infra of this.selectedInfraList) { + this.infraservice.deleteInfra(infra.uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + } + + isTestFailed: boolean = false; + testPassed: boolean = false; + //testConnection is to test the k8s connection by using kubeconfig + testConnection() { + this.testPassed = false; + this.isTestFailed = false; + if (this.newInfraForm.get('kubeconfig')?.valid) { + this.infraservice.testK8sConnection(this.newInfraForm.get('kubeconfig')?.value).subscribe(() => { + this.testPassed = true; + }, + err => { + this.isTestFailed = true; + this.testPassed = false; + this.errorMessage = err.error.message; + }); + } else { + this.errorMessage = "invalid kubeconfig input"; + this.testPassed = false; + } + } + + isCreatedFailed: boolean = false; + //createNewInfra is to create an new infra + createNewInfra() { + this.isCreatedFailed = false; + var infraInfo = { + description: this.newInfraForm.get('description')?.value, + kubernetes_provider_info: { + kubeconfig_content: this.newInfraForm.get('kubeconfig')?.value, + registry_config_fate: { + use_registry: this.newInfraForm.get('use_registry')?.value, + registry: this.use_registry ? this.newInfraForm.get('registry')?.value?.trim() : "", + use_registry_secret: this.newInfraForm.get('use_registry_secret')?.value, + registry_secret_config: { + server_url: this.use_registry_secret ? this.newInfraForm.get('server_url')?.value?.trim() : "", + username: this.use_registry_secret ? this.newInfraForm.get('username')?.value?.trim() : "", + password: this.use_registry_secret ? this.newInfraForm.get('password')?.value?.trim() : "", + }, + } + }, + name: this.newInfraForm.get('infraname')?.value, + type: this.newInfraForm.get('type')?.value, + } + + this.infraservice.createInfra(infraInfo) + .subscribe( + data => { + this.isCreatedFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + + } + + //submitDisable returns if disabled the submit button of create a new infra + get submitDisable() { + var registry_secret_valid = false; + if (this.use_registry_secret) { + registry_secret_valid = (this.newInfraForm.get('username')?.value?.trim() !== '') && (this.newInfraForm.get('password')?.value?.trim() !== '') && (!this.newInfraForm.controls['server_url'].errors) + } else { + registry_secret_valid = true + } + var registry_valid = false; + if (this.use_registry) { + registry_valid = (this.registry && this.registry != '') + } else { + registry_valid = true + } + var basics_valid = !this.newInfraForm.controls['infraname'].errors && !this.newInfraForm.controls['type'].errors + return !(this.testPassed && registry_secret_valid && registry_valid && basics_valid) + } + + //refresh is for refresh button + refresh() { + this.showInfraList(); + } + + //onKubeconfigChange is triggered when the input of kubeconfig is changed + onKubeconfigChange(val: any) { + this.testPassed = false; + this.isTestFailed = false; + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //validURL is to validate URL is valid + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + //server_url_suggestion to return the suggested server url based on the provided registry + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registry === '' || this.registry === null) { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registry.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + //valid_server_url return if the the server url is valid or not + get valid_server_url() { + return !this.newInfraForm.controls['server_url'].errors + } + +} diff --git a/frontend/src/app/view/login/login.component.html b/frontend/src/app/view/login/login.component.html new file mode 100644 index 00000000..e5074f03 --- /dev/null +++ b/frontend/src/app/view/login/login.component.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/frontend/src/app/view/login/login.component.scss b/frontend/src/app/view/login/login.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/view/login/login.component.spec.ts b/frontend/src/app/view/login/login.component.spec.ts new file mode 100644 index 00000000..d2c0e6c8 --- /dev/null +++ b/frontend/src/app/view/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/login/login.component.ts b/frontend/src/app/view/login/login.component.ts new file mode 100644 index 00000000..71789b3f --- /dev/null +++ b/frontend/src/app/view/login/login.component.ts @@ -0,0 +1,90 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { compile } from 'src/utils/compile'; +import jwt_decode from 'jwt-decode'; +import { AuthService } from '../../services/common/auth.service'; +import { MessageService } from 'src/app/components/message/message.service'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit { + //form is login information form + form: any = { + username: null, + password: null + }; + constructor(private authService: AuthService, private router: Router, private $msg: MessageService) { } + + ngOnInit(): void { + const redirect = sessionStorage.getItem('lifecycleManager-redirect') + if (redirect) { + this.$msg.warning('serverMessage.default401') + } + } + + username: string = ""; + password: string = ""; + loading = false; + submitted = false; + isLoggedIn = false; + isLoginFailed = false; + errorMessage = "Service Error!"; + decode: any; + //onSubmit is to submit the login information + onSubmit(): void { + this.submitted = true; + this.loading = true; + const { username, password } = this.form; + this.authService.login(username, password) + .subscribe( + data => { + this.isLoginFailed = false; + this.isLoggedIn = true; + //decode JWT token + var token = data.data; + this.decode = jwt_decode(token); + // store username, id in seesion storage + const encryptName: string = compile(this.decode["name"]); + sessionStorage.setItem('username', encryptName); + sessionStorage.setItem('userId', this.decode["id"]); + // redirect pre page + const redirect = sessionStorage.getItem('lifecycleManager-redirect') + if (redirect) { + this.router.navigate([redirect]) + sessionStorage.removeItem('lifecycleManager-redirect') + try { + this.$msg.close() + } catch (error) { + + } + } else { + this.router.navigate(['']); + try { + this.$msg.close() + } catch (error) { + + } + } + }, + err => { + if (err.error.message) this.errorMessage = err.error.message + this.isLoginFailed = true; + } + ); + } +} diff --git a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.html b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.html new file mode 100644 index 00000000..bd7f7158 --- /dev/null +++ b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.html @@ -0,0 +1,93 @@ +
+ + + + + {{openflForm.get('name')?.errors?.emptyMessage || openflForm.get('name')?.errors?.message | translate}} + + {{'CommonlyUse.few' | + translate}}{{openflForm.get('name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' | + translate}} + {{'CommonlyUse.many' | + translate}}{{openflForm.get('name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' | + translate}} + + + + + + + + + {{openflForm.get('domain')?.errors?.emptyMessage || + openflForm.get('domain')?.errors?.message | translate}} + + + + + + + + +
+
+ + [ + ] +

+ {{openflForm.get('sample')?.errors?.emptyMessage || openflForm.get('sample')?.errors?.message | + translate}} +

+
+
+ + [ + ] +

+ {{openflForm.get('target')?.errors?.emptyMessage || openflForm.get('target')?.errors?.message | + translate}} +

+
+ + + + +
+ + + + + {{ + 'Openfl.pythonFileUploadErrorMessage'| translate}} + +
+

{{'DirectorNew.files'| translate}}

+
    +
  1. + {{file.name}} +
  2. +
+
+ + + + + {{ + 'Openfl.requirementFileUploadErrorMessage'| translate}} + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.scss b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.scss new file mode 100644 index 00000000..350af4b1 --- /dev/null +++ b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.scss @@ -0,0 +1,111 @@ +.t2{ + width: 100%; + height: 100px; +} +.suffixHelper { + margin-top: 13px; + margin-left: -0.3px; +} + +input { + width: 97%; +} +select{ + width: 150px; +} +.refreshbtn { + float: right; + margin-right: 12px; + margin-top: 24px; +} +ol { + &.file-list { + margin-left: 110px; + } +} +.clr-input-contaner{ + position: relative; + align-items: flex-start; + box-sizing: border-box; + color: rgb(102, 102, 102); + display: flex; + flex-direction: row; + flex-wrap :wrap; + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: normal; + line-height: 24px; + margin-left: -12px; + margin-right: -12px; + margin-top: 24px; + text-size-adjust: 100%; + width: 534px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + label { + display: block; + color: var(--clr-forms-label-color, #454545); + font-size: .65rem; + font-weight: var(--clr-forms-label-font-weight, 600); + line-height: .9rem; + } + input { + border: 0; + border-bottom: 1px solid #b3b3b3; + width: 70%; + padding-right: 30px; + box-sizing: border-box; + &:focus-visible { + border: 0; + border-bottom: 1px solid #0072a3; + outline: none ; + } + &:focus { + border: 0; + border-bottom: 1px solid #0072a3; + outline: none; + } + &.error { + border-bottom: 1px solid #c21d00; + } + } + .file { + // width: 200px; + flex: 1; + position: relative; + top: -10px; + left: 15px; + } + .error-standard { + color: #c21d00; + top: 4px; + right: 25px; + position: absolute; + } + .valid-errot{ + margin-top: 0px; + font-size: 0.55rem; + color: #c21d00; + margin-left: 100px; + position: absolute; + bottom: -24px; + } +} +.clr-textrea::ng-deep { + width: 490px; + // .clr-control-container [ + // margin-left: 60px; + + // ] + .t3 { + margin-left: 104px; + border: 1px solid #b3b3b3; + padding: 0; + height: 150px; + overflow: auto; + border-radius: 0.15rem; + } +} +.tooltip-icon { + float: right; +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.spec.ts b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.spec.ts new file mode 100644 index 00000000..15aa77d7 --- /dev/null +++ b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateOpenflComponent } from './create-openfl-fed.component'; + +describe('CreateOpenflComponent', () => { + let component: CreateOpenflComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CreateOpenflComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateOpenflComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts new file mode 100644 index 00000000..77deafb4 --- /dev/null +++ b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts @@ -0,0 +1,222 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { ValidatorGroup } from 'src/utils/validators' +import { OpenflPsotModel } from 'src/app/services/openfl/openfl-model-type' +import { OpenflService } from 'src/app/services/openfl/openfl.service' +import { Observable, throwError } from 'rxjs' + +@Component({ + selector: 'app-create-openfl', + templateUrl: './create-openfl-fed.component.html', + styleUrls: ['./create-openfl-fed.component.scss'] +}) +export class CreateOpenflComponent implements OnInit { + + constructor( + private fb: FormBuilder, + private openflService: OpenflService + ) { } + + ngOnInit(): void { + } + openflForm = this.fb.group( + ValidatorGroup([ + { + name: 'name', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'domain', + value: '', + type: ['fqdn'] + }, + { + name: 'customize', + value: false, + type: [''] + }, + { + name: 'target', + value: '', + type: ['number-list'] + }, + { + name: 'sample', + value: '', + type: ['number-list'] + }, + { + name: 'envoyYaml', + value: '', + type: ['require'] + } + ]) + ) + code: any + fileStatus = '' + requirementStatus = '' + requirements: any = {} + uploadFileList: any[] = [] + + + openflToggleChange() { + setTimeout(() => { + try { + this.setCodeMirror() + } catch (error) { + + } + }) + } + // Create rich text + setCodeMirror() { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + }) + // Listen to the rich text input and save it to the form + this.code.on('change', (cm: any) => { + this.code.save() + this.openflForm.get('envoyYaml')?.setValue(this.code.getValue()) + }) + } + // Form validation when customize is false + customizeFalseValidate() { + const validList = ['name', 'domain'] + const result = validList.every(el => { + const obj = this.openflForm.get(el) + return obj && obj.valid === true + }) + return result + } + + // Form validation when customize is true + customizeTrueValidate() { + return this.openflForm.valid && this.fileStatus === 'success' && this.requirementStatus !== 'error' + } + + uploadFileChange(e: any, fileType: string) { + if (fileType === 'py') { + if (e.target.files.length === 0) { + this.fileStatus = 'enabled' + this.uploadFileList = [] + } else { + if (!this.isPythonFile(e.target.files)) { + this.fileStatus = 'error' + } else { + for (let i = 0; i < e.target.files.length; i++) { + // Create a FileReader + const reader = new FileReader(); + // Read file + reader.readAsText(e.target.files[i], "UTF-8"); + reader.onload = (evt: any) => { + // Get file content + const fileString = evt.target.result; + this.uploadFileList.push({ + name: e.target.files[i].name, + content: fileString + }) + } + } + this.fileStatus = 'success' + } + } + } else { + this.requirementStatus = 'enabled' + if (e.target.files.length !== 0) { + if (this.isRequirementsFile(e.target.files)) { + const reader = new FileReader(); + reader.readAsText(e.target.files[0], "UTF-8"); + reader.onload = (evt: any) => { + // Get file content + const fileString = evt.target.result; + this.requirements = { + name: 'requirements.txt', + content: fileString + } + } + this.requirementStatus = 'success' + } else { + this.requirementStatus = 'error' + } + } else { + this.requirements = {} + } + } + } + + // Determine whether it is a python file + isPythonFile(files: any) { + let result = true + for (let i = 0; i < files.length; i++) { + const suffix = files[i].name.slice(-3) + if (suffix !== '.py') { + result = false + break + } + } + return result + } + + // Determine whether it is a requirements.txt + isRequirementsFile(files: any): boolean { + for (let i = 0; i < files.length; i++) { + if (files[i].name !== 'requirements.txt') { + return false + } + } + return true + } + + createNewOpenfl(): Observable { + const customize = this.openflForm.get('customize')?.value + const openflInfo: OpenflPsotModel = { + name: this.openflForm.get('name')?.value, + description: this.openflForm.get('description')?.value, + domain: this.openflForm.get('domain')?.value, + shard_descriptor_config: { + envoy_config_yaml: '', + python_files: {}, + sample_shape: [], + target_shape: [] + }, + use_customized_shard_descriptor: true + } + if (customize) { + if (this.customizeTrueValidate()) { + openflInfo.shard_descriptor_config.envoy_config_yaml = this.openflForm.get('envoyYaml')?.value + openflInfo.shard_descriptor_config.sample_shape = this.openflForm.get('sample')?.value.split(',') + openflInfo.shard_descriptor_config.target_shape = this.openflForm.get('target')?.value.split(',') + this.uploadFileList.forEach(el => { + openflInfo.shard_descriptor_config.python_files[el.name] = el.content + }) + if (this.requirements.name) { + openflInfo.shard_descriptor_config.python_files[this.requirements.name] = this.requirements.content + } + return this.openflService.createOpenflFederation(openflInfo) + } else { + return throwError('Verification failed') + } + } else { + if (this.customizeFalseValidate()) { + openflInfo.use_customized_shard_descriptor = false + return this.openflService.createOpenflFederation(openflInfo) + } else { + return throwError('Verification failed') + } + } + + } +} diff --git a/frontend/src/app/view/openfl/director-detail/director-detail.component.html b/frontend/src/app/view/openfl/director-detail/director-detail.component.html new file mode 100644 index 00000000..7a075997 --- /dev/null +++ b/frontend/src/app/view/openfl/director-detail/director-detail.component.html @@ -0,0 +1,199 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'DirectorDetail.detail'|translate}}

+ +
+
+ + + + + +
+
+ + +
+
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{directorDetail?.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{directorDetail?.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{directorDetail?.created_at | dateFormat}} +
  • +
  • + UUID: + {{directorDetail?.uuid}} +
  • +
  • + {{'NewCluster.namespace'|translate}}: + {{directorDetail?.namespace}} +
  • +
  • + {{'CommonlyUse.status'|translate}}: + {{constantGather('director', + directorDetail?.status).name | translate}} +
  • +
  • + {{'ExchangeDetail.infraProviderName'|translate}}: + {{directorDetail?.infra_provider_name}}{{directorDetail?.infra_provider_name}} +
  • +
  • + {{'ExchangeDetail.endpointName'|translate}}: + {{directorDetail?.endpoint_name}}{{directorDetail?.endpoint_name}} +
  • +
  • + {{'ExchangeDetail.clusterUuid'|translate}}: + {{directorDetail?.cluster_uuid}} +
  • +
+
+
+
+
{{'ExchangeDetail.accessInfo'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.Port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + + + + Jupyter Notebook + + {{acces.name}} + + {{acces.host}} + {{acces.port}} + {{acces.service_type ? acces.service_type : 'N/A'}} + {{acces.fqdn ? acces.fqdn : 'N/A'}} + {{acces.tls}} + + +
+
+
+
+
{{'ExchangeDetail.certificateInformation'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'ExchangeDetail.bindingMode'|translate}} + {{'ExchangeDetail.commonName'|translate}} + UUID + + {{"DirectorDetail.directorServerCertInfo"|translate}} + {{constantGather('bindType', + directorDetail?.director_server_cert_info?.binding_mode).name |translate}} + {{directorDetail?.director_server_cert_info?.common_name ? + directorDetail?.director_server_cert_info?.common_name : 'N/A'}} + {{directorDetail?.director_server_cert_info?.uuid ? + directorDetail?.director_server_cert_info?.uuid : 'N/A'}} + + + {{"DirectorDetail.jupyterClientCertInfo"|translate}} + {{constantGather('bindType', directorDetail?.jupyter_client_cert_info?.binding_mode).name + |translate}} + {{directorDetail?.jupyter_client_cert_info?.common_name ? + directorDetail?.jupyter_client_cert_info?.common_name : 'N/A'}} + {{directorDetail?.jupyter_client_cert_info?.uuid ? + directorDetail?.jupyter_client_cert_info?.uuid : 'N/A'}} + + +
+
+
+
{{'ExchangeDetail.deploymentYaml'|translate}}:
+
+
    +
  • + +
  • +
+
+
+
+
+
+ + + + + + + +
+
+
+ + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/view/openfl/director-detail/director-detail.component.scss b/frontend/src/app/view/openfl/director-detail/director-detail.component.scss new file mode 100644 index 00000000..6d4ce54d --- /dev/null +++ b/frontend/src/app/view/openfl/director-detail/director-detail.component.scss @@ -0,0 +1,166 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } +} +.card1 { + width: 100%; + min-width: 1200px; + ol { + h6 { + margin: 0px; + } + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + b { + display: inline-block; + width: 300px; + } + } + .clr-form-control { + margin-top: 5px; + .clr-textarea-wrapper { + width: 600px; + } + textarea { + width: 600px; + height: 300px; + } + } + } + &.ol { + li { + border-bottom: 1px solid #999; + padding-bottom: 10px; + } + } + &.ol-ul { + margin-left: 30px; + li { + border-bottom: none; + padding-bottom: 0px; + span { + font-size: 12px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + &.child { + li { + span{ + font-size: 12px; + } + } + } + + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} +.pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255, 255, 255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; +} +.card { + min-width: 1200px; + .alert { + width: 30%; + } + .list { + width: 100%; + &.first { + li::ng-deep { + span:first-of-type { + display: inline-block; + width: 250px; + } + } + } + li::ng-deep { + span:first-of-type { + display: inline-block; + // width: 250px; + &.bind-mode { + width: 115px; + } + } + .clr-form-control { + margin-top: 5px; + textarea { + width: 600px; + height: 300px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li { + height: 800px; + width: 800px; + } + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + + } +} + +.deployment-yaml { + color: #333; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/director-detail/director-detail.component.spec.ts b/frontend/src/app/view/openfl/director-detail/director-detail.component.spec.ts new file mode 100644 index 00000000..3637ddc8 --- /dev/null +++ b/frontend/src/app/view/openfl/director-detail/director-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DirectorDetailComponent } from './director-detail.component'; + +describe('ExchangeDetailComponent', () => { + let component: DirectorDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DirectorDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DirectorDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/openfl/director-detail/director-detail.component.ts b/frontend/src/app/view/openfl/director-detail/director-detail.component.ts new file mode 100644 index 00000000..a389111f --- /dev/null +++ b/frontend/src/app/view/openfl/director-detail/director-detail.component.ts @@ -0,0 +1,161 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router' +import { OpenflService } from 'src/app/services/openfl/openfl.service'; +import { constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-director-detail', + templateUrl: './director-detail.component.html', + styleUrls: ['./director-detail.component.scss'] +}) +export class DirectorDetailComponent implements OnInit { + isShowDetailFailed = false + isPageLoading = true + errorMessage = "Service Error!" + uuid = '' + director_uuid = '' + constantGather = constantGather + directorDetail: any = {} + openDeleteModal = false + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + accessInfoList: { [key: string]: any }[] = [] + constructor(private route: ActivatedRoute, private router: Router, private openflService: OpenflService) { } + ngOnInit(): void { + this.getDirectorDetail() + } + code: any + overview = true + get isOverview() { + return this.overview + } + set isOverview(value) { + if (value) { + const yamlHTML = document.getElementById('yaml') as any + if (this.directorDetail.deployment_yaml) { + this.createCustomize(yamlHTML, this.directorDetail.deployment_yaml) + } + } else { + this.code = null + } + this.overview = value + } + + getDirectorDetail() { + this.openDeleteModal = false + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.isShowDetailFailed = false + this.isPageLoading = true + this.errorMessage = '' + this.uuid = '' + this.director_uuid = '' + this.route.params.subscribe( + value => { + this.uuid = value.id + this.director_uuid = value.director_uuid + if (this.uuid && this.director_uuid) { + this.openflService.getDirectorInfo(value.id, value.director_uuid).subscribe( + data => { + this.directorDetail = data.data + const value = data.data.deployment_yaml + setTimeout(() => { + const yamlHTML = document.getElementById('yaml') as any + if (!yamlHTML) { + try { + const timer = setInterval(() => { + const yamlHTML = document.getElementById('yaml') as any + if (yamlHTML) { + this.createCustomize(yamlHTML, value) + clearInterval(timer) + } + }, 100) + } catch (error) { + } + } else { + try { + this.createCustomize(yamlHTML, value) + } catch (error) { + } + } + }) + for (const key in data.data.access_info) { + const obj: any = { + name: key, + } + const value = data.data.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.accessInfoList.push(obj) + } + this.isPageLoading = false + }, + err => { + this.isPageLoading = false + this.errorMessage = err.error.message; + this.isShowDetailFailed = true + } + ) + } + } + ) + + } + + // create customize + createCustomize(yamlHTML: any, data: string) { + if (!this.code) { + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + } + setTimeout(() => { + this.code.setValue(data) + this.isPageLoading = false + }) + } + + //refresh is for refresh button + refresh() { + this.accessInfoList = [] + this.getDirectorDetail() + } + + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.openDeleteModal = true + this.forceRemove = false + } + get accessInfo() { + return JSON.stringify(this.directorDetail.access_info) === '{}' + } + deleteType = 'director' + forceRemove = false + confirmDelete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.openflService.deleteDirector(this.uuid, this.director_uuid, this.forceRemove) + .subscribe(() => { + this.router.navigate(['/federation/openfl', this.uuid]); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + + } + toLink(item: any) { + window.open( + `http://${item.host}:${item.port}`, '_blank' + ) + } +} diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.html b/frontend/src/app/view/openfl/director-new/director-new.component.html new file mode 100644 index 00000000..e1645d91 --- /dev/null +++ b/frontend/src/app/view/openfl/director-new/director-new.component.html @@ -0,0 +1,298 @@ +
+ <<{{'CommonlyUse.back'| translate}} +
+

{{'DirectorNew.name' | translate}}

+
+
+ + + {{'NewCluster.basicInformation'| translate}} + + + + + + {{ 'validator.empty' | translate}} + + + + + + + + + + + + {{'NewCluster.selectEndpoint'| translate}} + + + + + {{errorMessage}} + + +
{{'EndpointNew.currentSelection'|translate}}:    {{selectedEndpoint?.name}}
+ + + {{'NewCluster.noEndpoint'| translate}}{{'EndpointNew.clickHere'| translate}} + + + + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.description'| translate}} + {{'CommonlyUse.type'| translate}} + {{'CommonlyUse.creationTime'| translate}} + {{'EndpointMg.infraName'|translate}} + {{'EndpointMg.endpointURL'|translate}} + {{'CommonlyUse.status'| translate}} + + {{endpoint?.name}} + {{endpoint?.description}} + {{endpoint?.type}} + {{endpoint?.created_at | date : "medium"}} + {{endpoint?.infra_provider_name}} + {{endpoint?.kubefate_host}} + {{constantGather('endpointstatus', endpoint.status).name | translate}} + + + + +
+
+ + + {{'NewCluster.selectChart'| translate}} + + + + + {{errorMessage}} + + + + + + + + + + + + {{'NewCluster.setNamespace'|translate}} + + + + + + + + + + + + {{'NewCluster.selectCertificates'|translate}} + + + + + + + + + + + + +
+
1. {{'DirectorNew.directorServerCertificate'|translate}}:
+ + + + + + + + + + + +
+
2. {{'DirectorNew.certificate'|translate}}:
+ + + + + + + + + + + +
+ + +
+
+ + + {{'DirectorNew.jupyterNotebook'|translate}} + + + + + + + + + + + + {{'ExchangeNew.chooseServiceType'|translate}} + + + + + + + + + + + + + + + + + + + {{'DirectorNew.configuration' | translate}} + + +
    + + + + + + + +
      + + + + {{'InfraProvider.imageName'|translate}}:    + {{registryConfig}} + federatedai/myimage:latest + + + {{'validator.empty'| translate}} + +
    +
    +
+
    + + + + + + + +
      +
    • + + + + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + {{'validator.internet'| translate}} + {{'InfraProvider.urlSuggestion'|translate}} {{server_url_suggestion}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    • + + + + {{'validator.empty'| translate}} + + +
    • +
    +
+
+ +
+ + + {{'PSP.pspConfigTitle' | translate}} + + + + + + + + + + + + + + + {{'NewCluster.checkYAML'|translate}} + + + +
+ + + + + + + + {{errorMessage}} + + +
+ + {{'CommonlyUse.submitting' | translate}} ... + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.scss b/frontend/src/app/view/openfl/director-new/director-new.component.scss new file mode 100644 index 00000000..63e40374 --- /dev/null +++ b/frontend/src/app/view/openfl/director-new/director-new.component.scss @@ -0,0 +1,59 @@ +.alert { + width: 30%; +} +li span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; +} +.t2{ + width: 300px; + height: 100px; +} +.yamlbtn { + display: flex; + float: left; + margin-bottom: 0; + margin-right: 100%; +} +.t2{ + width: 600px; + height: 100px; +} +.t3{ + width: 500px; + height: 400px; +} +.no-warp::ng-deep { + label { + margin-right: 15px; + line-height: 24px; + } + flex-direction: row; + align-items: center; +} +.yaml { + font-weight: 700; + margin: 10px 0; +} +.yaml-warp::ng-deep { + .clr-form-control { + .clr-textarea-wrapper { + width: 600px; + } + } + .clr-accordion-status { + display: flex; + } +} +form { + .clr-row-password { + flex-direction: row; + align-items: center; + label { + margin-right: 20px; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.spec.ts b/frontend/src/app/view/openfl/director-new/director-new.component.spec.ts new file mode 100644 index 00000000..8cbf0265 --- /dev/null +++ b/frontend/src/app/view/openfl/director-new/director-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DirectorNewComponent } from './director-new.component'; + +describe('ExchangeNewComponent', () => { + let component: DirectorNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DirectorNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DirectorNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.ts b/frontend/src/app/view/openfl/director-new/director-new.component.ts new file mode 100644 index 00000000..d5a479c7 --- /dev/null +++ b/frontend/src/app/view/openfl/director-new/director-new.component.ts @@ -0,0 +1,486 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EndpointService } from 'src/app/services/common/endpoint.service'; +import { OpenflService } from 'src/app/services/openfl/openfl.service' +import { ENDPOINTSTATUS, CHARTTYPE, constantGather } from 'src/utils/constant'; +import { ChartService } from 'src/app/services/common/chart.service'; +import { ValidatorGroup } from 'src/utils/validators' +import { InfraService } from 'src/app/services/common/infra.service'; +import { DirectorModel } from 'src/app/services/openfl/openfl-model-type'; +import { EndpointType } from 'src/app/view/endpoint/endpoint-model' + +@Component({ + selector: 'app-exchange-new', + templateUrl: './director-new.component.html', + styleUrls: ['./director-new.component.scss'] +}) +export class DirectorNewComponent implements OnInit { + form: FormGroup; + // The utility class is bound to this + endpointStatus = ENDPOINTSTATUS; + chartType = CHARTTYPE; + // bind the utility function to this, + // Display the corresponding string according to the value returned by the constantGather backend + constantGather = constantGather + + // Display error related properties + errorMessage: any; + isShowEndpointFailed: boolean = false; + isShowInfraDetailFailed: boolean = false; + isShowChartFailed: boolean = false; + isGenerateFailed = false; + isCreatedFailed = false; + + // Action related properties + isPageLoading: boolean = true; + noEndpoint = false; + openInfraModal: boolean = false; + use_cert: boolean = false; + skip_cert: boolean = false; + endpointSelectOk: boolean = false; + isGenerateSubmit = false; + isCreatedSubmit = false; + + // Id + openfl_uuid = String(this.route.snapshot.paramMap.get('id')); + infraUUID: any = "" + + + // The currently selected endpoint object + endpoint!: EndpointType | null + endpointlist: any = []; + chartlist: any = []; + + // Intermediate amount + infraConfigDetail: any; + hasRegistry = false; + hasRegistrySecret = false; + registrySecretConfig: any; + // CodeMirror instance + codeMirror: any + + get selectedEndpoint() { + return this.endpoint + } + set selectedEndpoint(value) { + this.endpoint = value + if (value) { + this.onSelectEndpoint() + } + } + get useRegistrySecret() { + return this.form.controls['registry'].get('useRegistrySecret')?.value; + } + get useRegistry() { + return this.form.controls['registry'].get('useRegistry')?.value; + } + get registryConfig() { + return this.form.controls['registry'].get('registryConfig')?.value; + } + + get server_url_suggestion() { + var url_suggestion = ""; + var header = "https://" + if (this.registryConfig === "" || this.registryConfig === null || this.registryConfig === "-") { + url_suggestion = header + "index.docker.io/v1/" + } else { + var url = this.registryConfig.split('/')[0] + if (this.validURL(url)) { + url_suggestion = header + url + } else { + url_suggestion = header + "index.docker.io/v1/" + } + } + return url_suggestion + } + + get valid_server_url() { + return this.form.get('registry.server_url')?.valid && this.form.controls['registry'].get('server_url')?.value?.trim() != '' + } + + //cert_disabled is to validate the configuration of certificate section and disabled the 'Next' button if needed + get cert_disabled() { + return this.use_cert && (this.form.controls['certificate'].get('jupyter_client_cert_info_mode')?.value === 1 + || this.form.controls['certificate'].get('director_server_cert_info_mode')?.value === 1) + } + + get service_type_disabled() { + return !this.form.controls['serviceType'].get('serviceType')?.valid + } + + get registry_disabled() { + var registry_secret_valid: any = true; + if (this.useRegistrySecret) { + registry_secret_valid = (this.form.controls['registry'].get('username')?.value?.trim() != '') && (this.form.controls['registry'].get('password')?.value?.trim() != '') && this.form.get('registry.server_url')?.valid && (this.form.controls['registry'].get('server_url')?.value?.trim() != '') + } else { + registry_secret_valid = true + } + var registry_valid = true; + if (this.useRegistry) { + registry_valid = this.form.controls['registry'].get('registryConfig')?.value?.trim() != '' + } else { + registry_valid = true + } + return !(registry_secret_valid && registry_valid) + } + + //submitDisable returns if disabled the submit button of create a new director + get submitDisable() { + if (!this.form.valid || this.cert_disabled || this.service_type_disabled || !this.isGenerateSubmit || (this.isCreatedSubmit && !this.isCreatedFailed)) { + return true + } else { + return false + } + } + + constructor(private formBuilder: FormBuilder, + private router: Router, + private route: ActivatedRoute, + private endpointService: EndpointService, + private chartservice: ChartService, + private infraservice: InfraService, + private openflService: OpenflService + ) { + this.form = this.formBuilder.group({ + info: this.formBuilder.group({ + name: [''], + description: [''], + }), + endpoint: this.formBuilder.group({ + endpoint_uuid: [''] + }), + chart: this.formBuilder.group({ + chart_uuid: [''], + }), + namespace: this.formBuilder.group({ + namespace: ['openfl-director'], + }), + certificate: this.formBuilder.group({ + cert: ['skip'], + director_server_cert_info_mode: [1], + director_server_cert_info_uuid: [''], + director_server_cert_info_name: [''], + jupyter_client_cert_info_uuid: [''], + jupyter_client_cert_info_mode: [1], + jupyter_client_cert_info_name: [''] + }), + jupyter: this.formBuilder.group( + ValidatorGroup([ + { + name: 'password', + type: ['require'], + value: '' + } + ]) + ), + serviceType: this.formBuilder.group({ + serviceType: [null], + }), + registry: this.formBuilder.group( + ValidatorGroup([ + { + name: 'useRegistry', + type: [''], + value: false + }, + { + name: 'useRegistrySecret', + type: [''], + value: false + }, + { + name: 'registryConfig', + type: [''], + value: '' + }, + { + name: 'server_url', + type: ['internet'], + value: '' + }, + { + name: 'username', + type: [''], + value: '' + }, + { + name: 'password', + type: [''], + value: '' + }, + ]) + ), + psp: this.formBuilder.group({ + enablePSP: [false], + }), + yaml: this.formBuilder.group({ + yaml: [''], + }) + }); + } + ngOnInit(): void { + this.showEndpointList(); + this.showChartList(); + } + // Get Endpoint List + showEndpointList() { + this.isShowEndpointFailed = false; + this.isPageLoading = true; + this.endpointService.getEndpointList() + .subscribe((data: any) => { + this.endpointlist = data.data.filter((el: any) => { if (el.status === 2) return el }); + if (this.endpointlist.length === 0) { + this.noEndpoint = true; + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowEndpointFailed = true; + this.isPageLoading = false; + }); + } + // Get Chart List + showChartList() { + this.chartlist = []; + this.isPageLoading = true; + this.isShowChartFailed = false; + this.chartservice.getChartList() + .subscribe((data: any) => { + if (data.data) { + for (let chart of data.data) { + if (chart.type === 3) { + this.chartlist.push(chart); + } + } + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowChartFailed = true; + this.isPageLoading = false; + }); + } + //setRadioDisplay is trigger when the selection of certificate's mode is changed + setRadioDisplay(val: any) { + if (val == "use") { + this.use_cert = true; + this.skip_cert = false; + } + if (val == "skip") { + this.use_cert = false; + this.skip_cert = true; + this.form.get('certificate')?.get('director_server_cert_info_mode')?.setValue(1); + this.form.get('certificate')?.get('jupyter_client_cert_info_mode')?.setValue(1); + } + } + + // initYAMLEditorByEvent is triggered when clicking the yaml textarea(if there is no highlight) to reinitialize the YAML editor + initYAMLEditorByEvent(event: any) { + if (event && event.target && event.target.value) { + this.isGenerateSubmit = true; + this.initCodeMirror(event.target.value) + } + } + + // initCodeMirror is to initialize Code Mirror YAML editor + initCodeMirror(yamlContent: any) { + const yamlHTML = document.getElementById('yaml') as any + this.codeMirror = window.CodeMirror.fromTextArea(yamlHTML, { + value: yamlContent, + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + }) + this.codeMirror.on('change', (cm: any) => { + this.codeMirror.save() + }) + this.hasYAMLTextAreaDOM = true + } + + hasYAMLTextAreaDOM = false + // onClickNext is triggered when clicking the 'Next' button before the step 'YAML' + onClickNext() { + this.hasYAMLTextAreaDOM = false; + // when the form is reset by the clarity stepper component and the step 'YAML' is collapsed, the DOM of step 'YAML' will be cleared. + // So we need to check if we need to initialized code mirror window in the next step (step 'YAML) + if (document.getElementById('yaml') !== null) { + this.hasYAMLTextAreaDOM = true; + } + } + + get editorModeHelperMessage() { + return this.codeMirror && !this.hasYAMLTextAreaDOM && this.form.controls['yaml'].get('yaml')?.value + } + + //reset form when selection change + onSelectEndpoint() { + if (this.selectedEndpoint) { + this.endpointSelectOk = true; + this.form.get('endpoint')?.get('endpoint_uuid')?.setValue(this.selectedEndpoint.uuid); + this.infraUUID = this.endpoint?.infra_provider_uuid + this.showInfraDetail(this.infraUUID) + } + } + // Get Infran Detail + showInfraDetail(uuid: string) { + this.isShowInfraDetailFailed = false; + this.infraservice.getInfraDetail(uuid) + .subscribe((data: any) => { + this.infraConfigDetail = data.data.kubernetes_provider_info; + this.hasRegistry = this.infraConfigDetail.registry_config_fate.use_registry + this.hasRegistrySecret = this.infraConfigDetail.registry_config_fate.use_registry_secret + this.registrySecretConfig = this.infraConfigDetail.registry_config_fate.registry_secret_config + this.form.get('registry')?.get('useRegistrySecret')?.setValue(this.hasRegistrySecret); + this.form.get('registry')?.get('useRegistry')?.setValue(this.hasRegistry); + + this.change_use_registry() + this.change_registry_secret() + }, + err => { + this.errorMessage = err.error.message; + this.isShowInfraDetailFailed = true; + } + ); + } + // Use Registry change + onChange_use_registry(val: any) { + this.change_use_registry() + this.selectChange(val) + } + + change_use_registry() { + if (this.useRegistry) { + this.form.get('registry')?.get('registryConfig')?.setValue(this.infraConfigDetail.registry_config_fate.registry); + } else { + this.form.get('registry')?.get('registryConfig')?.setValue("-"); + } + } + // Use Registry Secret change + onChange_use_registry_secret(val: any) { + this.change_registry_secret() + this.selectChange(val) + } + + change_registry_secret() { + if (this.useRegistrySecret) { + this.form.get('registry')?.get('server_url')?.setValue(this.registrySecretConfig.server_url); + this.form.get('registry')?.get('username')?.setValue(this.registrySecretConfig.username); + this.form.get('registry')?.get('password')?.setValue(this.registrySecretConfig.password); + } + else { + this.form.get('registry')?.get('server_url')?.setValue("https://x"); + this.form.get('registry')?.get('username')?.setValue("-"); + this.form.get('registry')?.get('password')?.setValue("-"); + } + } + + formResetChild(child: string) { + this.form.controls[child].reset(); + this.form.controls[child].reset(); + if (child === 'yaml' && this.codeMirror) { + this.codeMirror.setValue('') + } + } + + validURL(str: string) { + var pattern = new RegExp( + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + return !!pattern.test(str); + } + + // After the content of the previous step has changed + selectChange(val: any) { + this.formResetChild('yaml'); + this.isGenerateSubmit = false; + this.isGenerateFailed = false; + } + + get enablePSP() { + return this.form.controls['psp'].get('enablePSP')?.value; + } + + //generateYaml() is to get the exchange initial yaml based on the configuration provided by user + generateYaml() { + const yamlHTML = document.getElementById('yaml') as any + this.isGenerateSubmit = true; + var chart_id = this.form.controls['chart'].get('chart_uuid')?.value; + var namespace = this.form.controls['namespace'].get('namespace')?.value; + var name = this.form.controls['info'].get('name')?.value; + var service_type = Number(this.form.controls['serviceType'].get('serviceType')?.value); + var password = this.form.controls['jupyter'].get('password')?.value; + if (namespace === '') namespace = 'openfl-director'; + this.openflService.getDirectorYaml(this.openfl_uuid, password, chart_id, namespace, name, service_type, this.registryConfig, this.useRegistry, this.useRegistrySecret, this.enablePSP).subscribe( + data => { + this.form.get('yaml')?.get('yaml')?.setValue(data.data); + // if code mirror object and yaml DOM are existing, just set value to the code mirror object + // else initialize code mirror object and yaml editor window + if (this.codeMirror && this.hasYAMLTextAreaDOM) { + this.codeMirror.setValue(data.data) + } else { + this.initCodeMirror(data.data) + } + this.isGenerateFailed = false; + }, + err => { + this.errorMessage = err.error.message; + this.isGenerateFailed = true; + } + ) + } + + createNewOpenfl() { + this.isCreatedFailed = false; + this.isCreatedSubmit = true; + + const directorInfo: DirectorModel = { + chart_uuid: this.form.controls['chart'].get('chart_uuid')?.value, + deployment_yaml: this.codeMirror.getTextArea().value, + description: this.form.controls['info'].get('description')?.value, + endpoint_uuid: this.form.controls['endpoint'].get('endpoint_uuid')?.value, + federation_uuid: this.openfl_uuid, + jupyter_password: this.form.controls['jupyter'].get('password')?.value, + service_type: +this.form.controls['serviceType'].get('serviceType')?.value, + registry_config: { + use_registry: this.useRegistry, + use_registry_secret: this.useRegistrySecret, + registry: this.useRegistry ? this.registryConfig : "", + registry_secret_config: { + server_url: this.useRegistrySecret ? this.form.controls['registry'].get('server_url')?.value?.trim() : "", + username: this.useRegistrySecret ? this.form.controls['registry'].get('username')?.value?.trim() : "", + password: this.useRegistrySecret ? this.form.controls['registry'].get('password')?.value?.trim() : "", + } + }, + director_server_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('director_server_cert_info_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('director_server_cert_info_uuid')?.value, + }, + jupyter_client_cert_info: { + binding_mode: Number(this.form.controls['certificate'].get('jupyter_client_cert_info_mode')?.value), + common_name: "", + uuid: this.form.controls['certificate'].get('jupyter_client_cert_info_uuid')?.value + }, + name: this.form.controls['info'].get('name')?.value, + namespace: this.form.controls['namespace'].get('namespace')?.value + } + this.openflService.createDirector(this.openfl_uuid, directorInfo) + .subscribe( + data => { + this.isCreatedFailed = false; + this.router.navigateByUrl('/federation/openfl/' + this.openfl_uuid) + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + } +} diff --git a/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.html b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.html new file mode 100644 index 00000000..e2b7a56c --- /dev/null +++ b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.html @@ -0,0 +1,167 @@ +
+ <<{{'CommonlyUse.back'|translate}} +

{{'EnvoyDetail.detail'|translate}}

+ +
+ +
+
+ + + + + +
+
+ + +
+
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{envoyDetail.name}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{envoyDetail.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{envoyDetail.created_at | dateFormat}} +
  • +
  • + UUID: + {{envoyDetail.uuid}} +
  • +
  • + {{'NewCluster.namespace'|translate}}: + {{envoyDetail.namespace}} +
  • +
  • + {{'FederationOpenFlDetail.token_name'|translate}}: + {{envoyDetail.token_name}} +
  • +
  • +
    + {{'FederationOpenFlDetail.token'|translate}}: + + + + +
    +
  • +
  • + {{'CommonlyUse.status'|translate}}: + {{constantGather('envoy', + envoyDetail.status).name | translate}} +
  • +
  • + {{'ExchangeDetail.infraProviderName'|translate}}: + {{envoyDetail.infra_provider_name}}{{envoyDetail.infra_provider_name}} +
  • +
  • + {{'ExchangeDetail.endpointName'|translate}}: + {{envoyDetail.endpoint_name}}{{envoyDetail.endpoint_name}} +
  • +
  • + {{'ExchangeDetail.clusterUuid'|translate}}: + {{envoyDetail.cluster_uuid}} +
  • +
  • + {{'FederationOpenFlDetail.labels'|translate}}: + {{item.key}}:{{item.value}} +
  • +
+
+
+
+
{{'ExchangeDetail.certificateInformation'|translate}}:
+
+ + {{'CommonlyUse.name'|translate}} + {{'ExchangeDetail.bindingMode'|translate}} + {{'ExchangeDetail.commonName'|translate}} + UUID + + + {{'EnvoyDetail.envoyClientCertInfo'|translate}} + {{constantGather('bindType', envoyDetail.envoy_client_cert_info?.binding_mode).name | + translate}} + {{envoyDetail.envoy_client_cert_info?.common_name ? + envoyDetail.envoy_client_cert_info?.common_name : 'N/A'}} + {{envoyDetail.envoy_client_cert_info?.uuid ? envoyDetail.envoy_client_cert_info?.uuid : + 'N/A'}} + + +
+
+
+
+ + + + + + + +
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.scss b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.scss new file mode 100644 index 00000000..55b299d0 --- /dev/null +++ b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.scss @@ -0,0 +1,153 @@ +.content-area { + position: relative; + &.hidden { + overflow: hidden; + } +} +.pageLoading-bac { + width: 100%; + height: 100%; + position: absolute; + background-color: rgba($color: rgb(255,255,255), $alpha: 1); + left: 0px; + top: 0px; + z-index: 99; +} +.card1 { + width: 100%; + min-width: 1200px; + ol { + h6 { + margin: 0px; + } + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + b { + display: inline-block; + width: 300px; + } + } + .clr-form-control { + margin-top: 5px; + .clr-textarea-wrapper { + width: 600px; + } + textarea { + width: 600px; + height: 300px; + } + } + } + &.ol { + li { + border-bottom: 1px solid #999; + padding-bottom: 10px; + } + } + &.ol-ul { + margin-left: 30px; + li { + border-bottom: none; + padding-bottom: 0px; + span { + font-size: 12px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li { + height: 800px; + width: 800px; + } + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + &.child { + font-size: 12px; + } + + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 250px; + margin-right: 10px; + line-height: 36px; + } + .t3{ + width: 1000px; + height: 500px; + } +} +.pageLoading { + position: absolute; + left: 50%; + top: 10%; + transform: translateX(-50%); + z-index: 100; +} +.card { + min-width: 1200px; + .alert { + width: 30%; + } + .list { + width: 100%; + li::ng-deep { + span:first-of-type { + display: inline-block; + // width: 200px; + &.bind-mode { + width: 115px; + } + } + .clr-form-control { + margin-top: 5px; + textarea { + width: 600px; + height: 300px; + } + } + } + input { + width: 100%; + } + select{ + width: 150px; + } + &.none { + list-style: none; + li::ng-deep { + textarea { + width: 600px; + height: 300px; + } + } + } + + } +} +.deployment-yaml { + color: #333; +} +.statusLabel { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.spec.ts b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.spec.ts new file mode 100644 index 00000000..94ff5a2c --- /dev/null +++ b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EnvoyDetailComponent } from './envoy-detail.component'; + +describe('ClusterDetailComponent', () => { + let component: EnvoyDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EnvoyDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EnvoyDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.ts b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.ts new file mode 100644 index 00000000..9c4cfdfe --- /dev/null +++ b/frontend/src/app/view/openfl/envoy-detail/envoy-detail.component.ts @@ -0,0 +1,122 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router' +import { FedService } from 'src/app/services/federation-fate/fed.service' +import { OpenflService } from 'src/app/services/openfl/openfl.service'; +import { constantGather } from 'src/utils/constant'; + +@Component({ + selector: 'app-envoy-detail', + templateUrl: './envoy-detail.component.html', + styleUrls: ['./envoy-detail.component.scss'] +}) +export class EnvoyDetailComponent implements OnInit { + isShowDetailFailed = false + isPageLoading = true + errorMessage = '' + uuid = '' + envoy_uuid = '' + deleteType = 'cluster' + forceRemove = false + constantGather = constantGather + envoyDetail: any = {} + openDeleteModal = false + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + labelsList: { [key: string]: any }[] = [] + constructor(private route: ActivatedRoute, private router: Router, private openflService: OpenflService) { } + ngOnInit(): void { + this.getEnvoyDetail() + } + code: any + overview = true + get isOverview() { + return this.overview + } + set isOverview(value) { + if (value) { + const yamlHTML = document.getElementById('yaml') as any + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + if (this.envoyDetail.deployment_yaml) { + this.code.setValue(this.envoyDetail.deployment_yaml) + } + } else { + this.code = null + } + this.overview = value + } + getEnvoyDetail() { + this.openDeleteModal = false + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.isShowDetailFailed = false + this.isPageLoading = true + this.errorMessage = '' + this.uuid = '' + this.envoy_uuid = '' + this.labelsList = [] + this.route.params.subscribe( + value => { + this.uuid = value.id + this.envoy_uuid = value.envoy_uuid + //test + this.isPageLoading = false + if (this.uuid && this.envoy_uuid) { + this.openflService.getEnvoyInfo(this.uuid, this.envoy_uuid).subscribe( + data => { + this.envoyDetail = data.data + for (const key in data.data.labels) { + const obj: any = { + key: key, + value: data.data.labels[key] + } + this.labelsList.push(obj) + } + this.isPageLoading = false + + }, + err => { + this.isPageLoading = false + this.errorMessage = err.error.message; + this.isShowDetailFailed = false + } + ) + } + } + ) + } + + //refresh is for refresh button + refresh() { + this.getEnvoyDetail() + } + + //openDeleteConfrimModal is to open the confirmaton modal of deletion and initialized the variables + openDeleteConfrimModal() { + this.openDeleteModal = true + this.forceRemove = false; + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + } + + confirmDelete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.openflService.deleteEnvoy(this.uuid, this.envoy_uuid, this.forceRemove) + .subscribe(() => { + this.router.navigate(['/federation/openfl', this.uuid]); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + + } +} diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.html b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.html new file mode 100644 index 00000000..a015dd2b --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.html @@ -0,0 +1,500 @@ +
+ <<{{'CommonlyUse.back'|translate}} +
+

{{'FederationOpenFlDetail.name'|translate}}

+ + + + {{errorMessage}} + + +
+
+
+ + +
+
+
    +
  • + {{'CommonlyUse.name'|translate}}: + {{openflFederationDetail?.name}} +
  • +
  • + {{'CommonlyUse.type'|translate}}: + {{openflFederationDetail?.type}} +
  • +
  • + {{'CommonlyUse.description'|translate}}: + {{openflFederationDetail?.description}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{openflFederationDetail?.created_at | dateFormat}} +
  • +
  • + {{'Federation.domain'|translate}}: + {{openflFederationDetail?.domain}} +
  • +
  • +
    + {{'Federation.customize'|translate}}: + + + + +
    +
  • +
+
    +
  • + {{'Federation.sample'|translate}}: + {{openflFederationDetail?.shard_descriptor_config.sample_shape|json}} +
  • +
  • + {{'Federation.target'|translate}}: + {{openflFederationDetail?.shard_descriptor_config.target_shape|json}} +
  • +
  • + {{'Federation.pythonFiles'|translate}}: + {{item}} +
  • +
  • + {{'Federation.envoyConfig'|translate}} +
    + +
    +
  • +
+ +
+
+

+ {{'FederationOpenFlDetail.director'| translate}} +

+ + + {{'FederationOpenFlDetail.nodirector'| translate}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{'FederationOpenFlDetail.directorName'|translate}}{{'CommonlyUse.description'|translate}}{{'CommonlyUse.creationTime'|translate}}{{'FederationDetail.accessInfo'| translate}}{{'EndpointMg.name'|translate}}{{'InfraProvider.name'|translate}}{{'NewCluster.namespace'| translate}}{{'CommonlyUse.status'|translate}}{{'CommonlyUse.action'|translate}}
{{director.name}}{{director.description}}{{director.created_at | dateFormat}} + + + + {{'CommonlyUse.name'|translate}} + {{'EndpointDetail.host'|translate}} + {{'CommonlyUse.port'|translate}} + {{'ExchangeNew.serviceType'|translate}} + FQDN + TLS + + + {{director.name}} + {{director.host}} + {{director.port}} + {{director.service_type ? director.service_type : 'N/A'}} + {{director.fqdn ? director.fqdn : 'N/A'}} + {{director.tls}} + + + + + {{director.endpoint_name}}{{director.infra_provider_name}}{{director.namespace}}{{constantGather('director', + director.status).name | translate}}{{'CommonlyUse.delete'|translate}}
+
+

+ {{'FederationOpenFlDetail.client'| translate}} +

+ + + + + +
+
+ {{filterString}} + + +
+
    +
  • + +
  • +
  • + + + + + +
  • +
  • + + +
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{'CommonlyUse.name'|translate}}{{'CommonlyUse.description'|translate}}{{'CommonlyUse.creationTime'|translate}}{{'EndpointMg.name'|translate}}{{'InfraProvider.name'|translate}}{{'NewCluster.namespace'|translate}}{{'FederationOpenFlDetail.token'|translate}}{{'CommonlyUse.status'|translate}}{{'FederationOpenFlDetail.labels'|translate}}{{'CommonlyUse.action'|translate}}
+ + {{envoy.name}}{{envoy.description}}{{envoy.created_at| dateFormat}}{{envoy.endpoint_name}}{{envoy.infra_provider_name}}{{envoy.namespace}}{{envoy.token_name}} + {{constantGather('envoy', + envoy.status).name | translate}} + + {{item.key}}:{{item.value}} + + {{envoy.labels[0].key}}:{{envoy.labels[0].value}} + {{envoy.labels[1].key}}:{{envoy.labels[1].value}} + ... +
+ {{item.key}}:{{item.value}} +
+
+
+ {{'CommonlyUse.delete'|translate}} +
+
+
+
+ + + + + + {{'FederationOpenFlDetail.nodirectorToken'| translate}} + + {{'CommonlyUse.name'|translate}} + UUID + {{'CommonlyUse.expirationDate'|translate}} + {{'FederationOpenFlDetail.token'|translate}} + {{'FederationOpenFlDetail.limit'|translate}} + {{'FederationOpenFlDetail.labels'|translate}} + {{'CommonlyUse.action'|translate}} + + + {{token.name}} + + + {{token.uuid}} + + + {{token.expired_at | dateFormat}} + + + + + + + + {{token.limit}} ({{token.used}} used) + + + {{item.key}}:{{item.value}} + + + {{token.labels[0].key}}:{{token.labels[0].value}} + {{token.labels[1].key}}:{{token.labels[1].value}} + ... + + + {{'CommonlyUse.delete'|translate}} + + + + {{token.name}} + +
+
    +
  • + {{'CommonlyUse.description'|translate}}: + {{token?.description}} +
  • +
  • + UUID: + {{token?.uuid}} +
  • +
  • + {{'FederationOpenFlDetail.token'|translate}}: + {{token?.token_str}} +
  • +
  • + {{'CommonlyUse.creationTime'|translate}}: + {{token.creation_time | dateFormat}} +
  • +
  • + {{'CommonlyUse.expirationDate'|translate}}: + {{token.expired_at | dateFormat}} +
  • +
  • + {{'FederationOpenFlDetail.limit'|translate}}: + {{token?.limit}}({{token.used}} used) +
  • +
  • + {{'FederationOpenFlDetail.labels'|translate}}: + {{item.key}}:{{item.value}} +
  • +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.scss b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.scss new file mode 100644 index 00000000..abb6dc5e --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.scss @@ -0,0 +1,182 @@ +.ellipsis { + cursor: pointer; +} +.card { + width: 700px; + &.card-width-auto { + width: auto; + padding: 10px; + margin: 0px; + } + .list { + input { + width: 100%; + } + select{ + width: 150px; + } + } + .t2{ + width: 100%; + height: 60px; + } + span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 10px; + line-height: 36px; + } + .list { + &.list-hide { + padding-top: 0px; + } + &.list-show { + padding-bottom: 0px; + margin-bottom: 0px; + } + } +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.exchangealert { + width: 500px; +} +.selectedEnvoy { + margin-right: 12px; + margin-top: 12px; +} +clr-toggle-container { + margin-top: 3px; +} +.sub { + margin-bottom: 12px; +} +table { + margin-top: 12px; + min-width: 800px; + thead { + border-bottom: 1px solid #ccc; + tr { + background-color: #fafafa; + border-bottom: 1px solid #999; + th { + flex: 1; + border: none; + padding: 8px 0px 8px 12px; + min-width: 120px; + &.checkbox-item { + width: 40px; + min-width: 40px; + padding: 0px; + margin: 0px; + flex: 0; + input { + width: 40px; + } + } + span { + display: flex; + flex: 1; + padding: 3px 0; + border-right: 1px solid #999; + } + &:last-of-type { + span { + border-right: none; + } + } + } + } + } + tbody { + tr{ + border-bottom: 1px solid #999; + td::ng-deep { + flex: 1; + border: none; + padding: 8px 12px; + min-width: 120px; + text-align: left; + .signpost { + .signpost-content-body { + padding: 20px 0px 0 0px; + } + } + } + td { + position: relative; + &.checkbox-item { + width: 40px; + min-width: 40px; + padding: 0px; + margin: 0px; + flex: 0; + input { + width: 40px; + } + } + .label-list { + position: absolute; + width: 202px; + top: -35px; + left: -88px; + background: #ffff; + border: 1px solid #ddd; + border-radius: 10px 10px 0 10px; + padding: 10px; + z-index: 1111; + margin-bottom: 17px; + } + .label-list::after { + content: ''; + display: block; + height: 0; + width: 0; + border-top: 16px solid #fff; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + position: absolute; + bottom: -12px; + right: -4px; + transform: rotate(-21deg) + } + .label-list::before { + content: ''; + display: block; + height: 0; + width: 0; + border-top: 17px solid #ddd; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + position: absolute; + bottom: -14px; + right: -5px; + transform: rotate(-21deg); + } + } + + } + } +} +tr { + display: flex; + flex: 1; + align-items: center; + &.none { + display: block; + text-align: center; + padding-bottom: 20px; + } +} +.access_info { + width: 300px; + height: 150px; + border: none; +} + +clr-datagrid { + min-width: 1500px; +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.spec.ts b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.spec.ts new file mode 100644 index 00000000..08f94989 --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FedDetailOpneFLComponent } from './fed-detail-openfl.component'; + +describe('FedDetailFateComponent', () => { + let component: FedDetailOpneFLComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FedDetailOpneFLComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FedDetailOpneFLComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts new file mode 100644 index 00000000..fb18940a --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts @@ -0,0 +1,589 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OpenflService } from 'src/app/services/openfl/openfl.service'; +import { LabelModel, EnvoyModel, DirectorInfoModel } from 'src/app/services/openfl/openfl-model-type' +import { ParticipantFATEStatus, ParticipantFATEType, constantGather } from 'src/utils/constant'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ValidatorGroup } from 'src/utils/validators' +import { CreateTokenType } from 'src/app/services/openfl/openfl-model-type' +import * as moment from 'moment' +@Component({ + selector: 'app-fed-detail-openfl', + templateUrl: './fed-detail-openfl.component.html', + styleUrls: ['./fed-detail-openfl.component.scss', './fed-detail-openfl.component2.scss', './fed-detail-openfl.component3.scss'] +}) +export class FedDetailOpneFLComponent implements OnInit { + + newTokenForm!: FormGroup + /* token related data */ + tokenList: any = [] + labelKey = '' + labelValue = '' + labelList: LabelModel[] = [] + // expired_at + date!: any + minDate = moment(Date.now()).format('YYYY-MM-DD') + // + showLabelListTop = 0 + + // token labels filter + searchList: { key: string, value: string }[] = [{ + key: '', + value: '' + }] + // Filters already added + filterString = '' + + /* Operation flag */ + newTokenModal = false + newTokenLoading = false + openflConfigFlag = false + createTokenFail = false + showSearchFlag = false + addLabelFlag = false + tokenLabelEnterflag = false + isShowOpenflDetailFailed: boolean = false; + isPageLoading: boolean = true; + isEnvoyFlag = true + isShowParticipantListFailed: boolean = false; + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + envoyConfigLoading = false + // CodeMirror Instance + code: any + + // The utility class is bound to this + participantFATEstatus = ParticipantFATEType; + participantFATEtype = ParticipantFATEStatus; + // bind the utility function to this, + // Display the corresponding string according to the value returned by the constantGather backend + constantGather = constantGather; + + /* director and envoy related data */ + director!: DirectorInfoModel; + directorAccessInfoList: { [key: string]: any }[] = [] + envoylist: EnvoyModel[] = []; + envoyAccessInfoList: { [key: string]: any }[] = [] + + // store all envoys + storageDataList: EnvoyModel[] = []; + + // openfl federation uuid + uuid = String(this.route.snapshot.paramMap.get('id')); + openflFederationDetail: any; + errorMessage = "Service Error!" + + // deleted content + deleteType: 'token' | 'director' | 'envoy' | 'federation' | 'multipleEnvoy' = 'token'; + deleteUUID: string = '' + forceRemove = false + // tabs flag + get isEnvoy() { + return this.isEnvoyFlag + } + set isEnvoy(value) { + this.isEnvoyFlag = value + this.filterString = '' + this.showSearchOptions() + this.showSearchFlag = false + } + get clientDisabled() { + if (this.director && this.director.status === 1) { + return false + } + return true + } + + get createTokenDisabled() { + return !this.newTokenForm.valid + } + + get submitFilterDisbabled() { + return this.searchList.every(el => { + if (el.key === '' || el.value === '') { + return false + } else { + return true + } + }) + } + constructor(private openflService: OpenflService, private router: Router, private route: ActivatedRoute, private fb: FormBuilder) { + this.newTokenForm = this.fb.group( + ValidatorGroup([ + { + name: 'tokenName', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + value: '', + type: [''] + }, + { + name: 'expirationDate', + value: '', + type: ['require'] + }, + { + name: 'limit', + value: '', + type: ['number'] + }, + + ]) + ) + } + + ngOnInit(): void { + this.isPageLoading = true; + this.showOpenflDetail(this.uuid); + this.showTokenList(this.uuid) + this.showParticipantDetail() + } + // get Openfl Detail + showOpenflDetail(uuid: string) { + this.isShowOpenflDetailFailed = false; + // get openfl detail + this.openflService.getOpenflFederationDetail(uuid) + .subscribe((data: any) => { + this.openflFederationDetail = data.data; + this.openflFederationDetail.fileList = [] + // Convert file object to array + for (const key in data.data.shard_descriptor_config.python_files) { + this.openflFederationDetail.fileList.push(key) + } + if (this.openflFederationDetail.use_customized_shard_descriptor) { + this.openflToggleChange() + } + }, + err => { + this.errorMessage = err.error.message; + this.isPageLoading = false; + this.isShowOpenflDetailFailed = true; + } + ); + } + + // get director and envoy list + showParticipantDetail() { + this.openflService.getParticipantInfo(this.uuid).subscribe( + data => { + this.director = data.data.director + this.envoylist = data.data.envoy + this.storageDataList = data.data.envoy + if (this.director) { + for (const key in this.director.access_info) { + const obj: any = { + name: key, + } + const value = this.director.access_info[key] + if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') { + for (const key2 in value) { + obj[key2] = value[key2] + } + } + this.directorAccessInfoList.push(obj) + } + } + if (this.envoylist) { + this.envoylist.forEach((envoy: any) => { + const labels = [] + for (const key in envoy.labels) { + const obj: any = { + key: key, + value: envoy.labels[key] + } + labels.push(obj) + } + envoy.labels = labels + const access_info: any[] = [] + if (envoy.access_info && this.hasAccessInfo(envoy.access_info)) { + for (const key in envoy.access_info) { + const obj = { + name: key, + value: envoy.access_info[key] + } + access_info.push(obj) + } + envoy.access_info = access_info + } + }) + } + }, + err => { + this.errorMessage = err.error.message; + this.isPageLoading = false; + this.isShowOpenflDetailFailed = true; + } + ) + } + // get token List + showTokenList(uuid: string) { + // this.isPageLoading = true; + this.isShowParticipantListFailed = false; + this.openflService.getTokenList(uuid) + .subscribe((data: any) => { + this.tokenList = [] + data.data?.forEach((el: any) => { + const labels = JSON.parse(JSON.stringify(el.labels)) + el.labels = [] + for (const key in labels) { + el.labels.push({ + key: key, + value: labels[key] + }) + } + this.tokenList.push(el) + }); + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isPageLoading = false; + this.isShowParticipantListFailed = true; + } + ); + } + + // add token label + addLabel() { + this.labelList.push({ + key: this.labelKey, + value: this.labelValue, + no: Date.now() + }) + this.labelValue = '' + this.labelKey = '' + this.addLabelFlag = false + } + // close new label + cancelLabel() { + this.labelValue = '' + this.labelKey = '' + this.addLabelFlag = false + } + delLabel(index: number) { + this.labelList.splice(index, 1) + } + // envoy label mouserEnter + mouseEnter(enovy: EnvoyModel) { + if (enovy.labels.length < 3) { + this.showLabelListTop = 0 + } else { + this.showLabelListTop = Math.ceil((enovy.labels.length - 3)) * 20 + } + enovy.showLabelListFlag = true + } + // submit token + createToken() { + const tokenInfo: CreateTokenType = { + description: this.newTokenForm.get('description')?.value, + expired_at: this.date, + labels: {}, + limit: this.newTokenForm.get('limit')?.value * 1, + name: this.newTokenForm.get('tokenName')?.value + } + this.labelList.forEach(el => [ + tokenInfo.labels[el.key] = el.value + ]) + if (this.labelKey && this.labelValue) { + tokenInfo.labels[this.labelKey] = this.labelValue + } + this.openflService.createTokenInfo(this.uuid, tokenInfo).subscribe( + data => { + this.showTokenList(this.uuid) + this.resetToken() + this.newTokenModal = false + }, + err => { + this.newTokenModal = true + this.createTokenFail = true + this.errorMessage = err.error.message + + } + ) + } + // reset token modal + resetToken() { + this.newTokenForm.reset() + this.date = new Date() + this.labelList = [] + this.createTokenFail = false + } + + // Judge whether the object is empty + hasAccessInfo(object: any): boolean { + return JSON.stringify(object) !== '{}' + } + + // create customize + createCustomize(yamlHTML: any) { + if (!this.code) { + this.code = window.CodeMirror.fromTextArea(yamlHTML, { + value: '', + mode: 'yaml', + lineNumbers: true, + indentUnit: 1, + lineWrapping: true, + tabSize: 2, + readOnly: true + }) + } + if (this.code) { + this.code.setValue(this.openflFederationDetail.shard_descriptor_config.envoy_config_yaml) + this.envoyConfigLoading = false + } + } + + // customize change + openflToggleChange() { + this.envoyConfigLoading = true + setTimeout(() => { + const yamlHTML = document.getElementById('yaml') as any + if (!yamlHTML) { + try { + const timer = setInterval(() => { + const yamlHTML = document.getElementById('yaml') as any + if (yamlHTML) { + this.createCustomize(yamlHTML) + clearInterval(timer) + } + }, 100) + } catch (error) { + } + } else { + try { + this.createCustomize(yamlHTML) + } catch (error) { + } + } + }); + } + + multipleDeletion = false; + openDeleteConfrimModal(type: 'token' | 'director' | 'envoy' | 'federation' | 'multipleEnvoy', item_uuid: string) { + this.deleteType = type; + this.deleteUUID = item_uuid + this.isDeleteFailed = false; + this.openDeleteModal = true; + this.isDeleteSubmit = false; + this.forceRemove = false; + this.multipleDeletion = type === 'multipleEnvoy' ? true : false; + if (this.seletedEnvoyList.length > 1) this.multipleDeletion = true; + } + + // delete submit + delete() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + if (this.deleteType === 'federation') { + this.openflService.deleteOpenflFederation(this.uuid).subscribe( + data => { + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.openDeleteModal = false + this.router.navigate(['/federation']) + }, + error => { + this.openDeleteModal = true + this.isDeleteSubmit = true; + this.isDeleteFailed = true; + this.errorMessage = error.error.message + } + ) + } else if (this.deleteType === 'token') { + this.openflService.deleteTokenInfo(this.uuid, this.deleteUUID).subscribe( + data => { + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.openDeleteModal = false + this.showTokenList(this.uuid) + }, + error => { + this.openDeleteModal = true + this.isDeleteSubmit = true; + this.isDeleteFailed = true; + this.errorMessage = error.error.message + } + ) + } else if (this.deleteType === 'director') { + this.openflService.deleteDirector(this.uuid, this.deleteUUID, this.forceRemove).subscribe( + data => { + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.openDeleteModal = false + this.showParticipantDetail() + }, + error => { + this.openDeleteModal = true + this.isDeleteSubmit = true; + this.isDeleteFailed = true; + this.errorMessage = error.error.message + } + ) + } else if (this.deleteType === 'envoy' && !this.multipleDeletion) { + this.openflService.deleteEnvoy(this.uuid, this.deleteUUID, this.forceRemove).subscribe( + data => { + this.isDeleteSubmit = false; + this.isDeleteFailed = false; + this.openDeleteModal = false + this.showParticipantDetail() + }, + error => { + this.openDeleteModal = true + this.isDeleteSubmit = true; + this.isDeleteFailed = true; + this.errorMessage = error.message + } + ) + } else if (this.deleteType === 'multipleEnvoy' || this.multipleDeletion) { + for (let envoy of this.seletedEnvoys) { + envoy.deleteFailed = false; + envoy.deleteSuccess = false; + this.isDeleteFailed = false; + envoy.deleteSubmit = true; + this.isDeleteSubmit = true; + this.openflService.deleteEnvoy(this.uuid, envoy.uuid, this.forceRemove).subscribe( + data => { + envoy.deleteFailed = false; + envoy.deleteSuccess = true; + this.isDeleteFailed = false; + }, + error => { + this.openDeleteModal = true + envoy.deleteFailed = true; + envoy.deleteSuccess = false; + this.isDeleteFailed = true; + envoy.errorMessage = error.message + } + ) + } + } + } + + // Select all logos + get allSelect() { + if (this.envoylist && this.envoylist.length > 0) { + return this.envoylist.every(el => el.selected === true) + } else { + return false + } + } + set allSelect(val) { + this.envoylist.forEach(el => el.selected = val) + } + + seletedEnvoyList: EnvoyModel[] = [] + get seletedEnvoys() { + this.seletedEnvoyList = [] + if (this.envoylist && this.envoylist.length > 0) { + for (const envoy of this.envoylist) { + if (envoy.selected && envoy.status != 2) { + this.seletedEnvoyList.push(envoy) + } + } + } + return this.seletedEnvoyList + } + + isDeleteEvonyAllSuccess = false; + get deleteEvonyAllSuccess() { + for (const envoy of this.seletedEnvoys) { + if (!envoy.deleteSuccess) { + this.isDeleteEvonyAllSuccess = false + return this.isDeleteEvonyAllSuccess + } + } + this.isDeleteEvonyAllSuccess = true + return this.isDeleteEvonyAllSuccess + } + + //refresh is for refresh button + refresh() { + this.showOpenflDetail(this.uuid) + this.showTokenList(this.uuid) + this.showParticipantDetail() + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + // Jump envoy details + toDetail(type: string, detailId: string, info: any) { + this.route.params.subscribe( + value => { + this.router.navigateByUrl(`federation/openfl/${value.id}/${type}/detail/${detailId}`) + } + ) + } + // show envoy filter UI + showSearchOptions() { + this.showSearchFlag = !this.showSearchFlag + this.searchList = [{ + key: '', + value: '' + }] + } + // reset envoy filter + recycleSearchOptions() { + this.showSearchFlag = false + this.filterString = '' + this.envoylist = this.storageDataList + this.searchList = [{ + key: '', + value: '' + }] + } + // add envoy search label + addSearchOption() { + this.searchList.push({ + key: '', + value: '' + }) + } + // delete envoy search label + delSearchOption(index: number) { + this.searchList.splice(index, 1) + } + // confirm filter + submitFilter() { + this.filterString = '' + const keys: string[] = [] + const values: string[] = [] + const filterEnvoyKeyList: EnvoyModel[] = [] + const filterEnvoyList: EnvoyModel[] = [] + this.searchList.forEach(el => { + keys.push(el.key) + values.push(el.value) + this.filterString += `${el.key}:${el.value},` + }) + this.storageDataList.filter(el => { + el.labels.forEach(label => { + if (keys.find(key => key === label.key)) { + if (!filterEnvoyKeyList.find(envoy => envoy.uuid === el.uuid)) { + filterEnvoyKeyList.push(el) + } + } + }) + }) + filterEnvoyKeyList.forEach(el => { + el.labels.forEach(label => { + if (values.find(value => value === label.value)) { + if (!filterEnvoyList.find(envoy => envoy.uuid === el.uuid)) { + filterEnvoyList.push(el) + } + } + }) + }) + this.envoylist = filterEnvoyList + this.showSearchFlag = false + } +} diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component2.scss b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component2.scss new file mode 100644 index 00000000..07f78d24 --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component2.scss @@ -0,0 +1,113 @@ + +clr-alert { + margin-top: 12px; + margin-bottom: 12px; +} +clr-tabs::ng-deep { + .tab-content { + .labels-filter { + position: relative; + height: 20px; + .search { + position: absolute; + top: 8px; + right: 14px; + cursor: pointer; + &.recycle { + right: 30px; + } + } + .search-input { + position: absolute; + border: none; + border-bottom: 1px solid #ddd; + top: 2px; + right: 10px; + padding-right: 36px; + background-color: #FAFAFA; + &:focus-visible { + border: 0; + border-bottom: 1px solid #ddd; + outline: none ; + } + &:focus { + border: 0; + border-bottom: 1px solid #ddd; + outline: none; + } + } + ul { + list-style: none; + position: absolute; + top: 35px; + right: 0px; + z-index: 9; + background: #ffff; + border: 1px solid #333; + li:first-of-type { + height: 20px; + .circle { + float: right; + margin-right: 10px; + margin-top: 3px; + cursor: pointer; + } + } + li:last-of-type { + height: 20px; + button { + float: right; + } + } + .clr-label-content { + border: 1px solid #eee; + padding: 10px 5px; + label { + margin-right: 10px; + } + input { + width: 108px; + margin-right: 5px; + outline: none; + border: none; + border-bottom: 1px solid #333; + } + .circle { + width: 20px; + height: 20px; + color: #77BE53; + cursor: pointer; + &.times { + width: 16px; + height: 16px; + color: #BF0000; + } + } + } + } + } + } + clr-datagrid { + clr-dg-column:last-of-type { + flex: none; + min-width: 100px; + width: 100px !important; + } + clr-dg-row { + clr-dg-cell:last-of-type { + flex: none; + min-width: 100px; + width: 100px !important; + } + } + } + .table { + tr { + th:last-of-type { + flex: none; + min-width: 100px; + width: 100px; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component3.scss b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component3.scss new file mode 100644 index 00000000..a4c178a4 --- /dev/null +++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component3.scss @@ -0,0 +1,89 @@ + +.modal-xl { + .clr-label-container { + margin-top: 24px; + label { + padding: 0px; + } + .clr-label-title { + .circle { + cursor: pointer; + margin-right: 5px; + } + i { + font-style: normal; + position: relative; + .times { + position: relative; + color: #bf0000; + right: -10px; + } + } + } + .clr-label-content { + margin-left: 122px; + border: 1px solid #eee; + padding: 10px 5px; + label { + margin-right: 10px; + } + input { + width: 108px; + margin-right: 5px; + outline: none; + border: none; + border-bottom: 1px solid #333; + } + .circle { + width: 20px; + height: 20px; + color: #77BE53; + cursor: pointer; + &.times { + width: 16px; + height: 16px; + color: #BF0000; + } + } + } + .clr-label-title { + display: flex; + align-items: center; + } + } +} +.customize { + display: flex; + align-items: center; + span { + display: inline-block; + } + &::ng-deep { + .clr-toggle-wrapper { + .clr-control-label { + &.true::before { + border-color: #5aa220 !important; + background-color: #5aa220; + } + &.false::before { + background-color: #8c8c8c; + color: #8c8c8c; + } + } + } + } +} +.yaml-wrap { + width: 600px; + height: 150px; + &::ng-deep { + .clr-form-control { + margin-top: 0px; + } + } +} +clr-password-container::ng-deep { + .clr-input-wrapper { + max-width: 7rem; + } +} \ No newline at end of file diff --git a/frontend/src/assets/.gitkeep b/frontend/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/assets/codemirror/codemirror.css b/frontend/src/assets/codemirror/codemirror.css new file mode 100644 index 00000000..3fafdcb5 --- /dev/null +++ b/frontend/src/assets/codemirror/codemirror.css @@ -0,0 +1,345 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 100% !important; + color: black; + direction: ltr; + width: 100%; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor .CodeMirror-line::selection, +.cm-fat-cursor .CodeMirror-line > span::selection, +.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; } +.cm-fat-cursor .CodeMirror-line::-moz-selection, +.cm-fat-cursor .CodeMirror-line > span::-moz-selection, +.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; } +.cm-fat-cursor { caret-color: transparent; } +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: 0; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 50px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -50px; margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + z-index: 0; +} +.CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; + outline: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -50px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre.CodeMirror-line, +.CodeMirror-wrap pre.CodeMirror-line-like { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/frontend/src/assets/codemirror/codemirror.js b/frontend/src/assets/codemirror/codemirror.js new file mode 100644 index 00000000..8686b788 --- /dev/null +++ b/frontend/src/assets/codemirror/codemirror.js @@ -0,0 +1,9849 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// This is CodeMirror (https://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.CodeMirror = factory()); +}(this, (function () { 'use strict'; + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); + var edge = /Edge\/(\d+)/.exec(userAgent); + var ie = ie_upto10 || ie_11up || edge; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); + var webkit = !edge && /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = !edge && /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = safari && (/Mobile\/\w+/.test(userAgent) || navigator.maxTouchPoints > 2); + var android = /Android/.test(userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var chromeOS = /\bCrOS\b/.test(userAgent); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) { presto_version = Number(presto_version[1]); } + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } + + var rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + { e.removeChild(e.firstChild); } + return e + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e) + } + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) { e.className = className; } + if (style) { e.style.cssText = style; } + if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } + else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } + return e + } + // wrapper for elt, which removes the elt from the accessibility tree + function eltP(tag, content, className, style) { + var e = elt(tag, content, className, style); + e.setAttribute("role", "presentation"); + return e + } + + var range; + if (document.createRange) { range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r + }; } + else { range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r + }; } + + function contains(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + { child = child.parentNode; } + if (parent.contains) + { return parent.contains(child) } + do { + if (child.nodeType == 11) { child = child.host; } + if (child == parent) { return true } + } while (child = child.parentNode) + } + + function activeElt() { + // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. + // IE < 10 will throw when accessed while the page is loading or in an iframe. + // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. + var activeElement; + try { + activeElement = document.activeElement; + } catch(e) { + activeElement = document.body || null; + } + while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) + { activeElement = activeElement.shadowRoot.activeElement; } + return activeElement + } + + function addClass(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } + } + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } + return b + } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } + else if (ie) // Suppress mysterious IE10 errors + { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args)} + } + + function copyObj(obj, target, overwrite) { + if (!target) { target = {}; } + for (var prop in obj) + { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + { target[prop] = obj[prop]; } } + return target + } + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) { end = string.length; } + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + { return n + (end - i) } + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + } + + var Delayed = function() { + this.id = null; + this.f = null; + this.time = 0; + this.handler = bind(this.onTimeout, this); + }; + Delayed.prototype.onTimeout = function (self) { + self.id = 0; + if (self.time <= +new Date) { + self.f(); + } else { + setTimeout(self.handler, self.time - +new Date); + } + }; + Delayed.prototype.set = function (ms, f) { + this.f = f; + var time = +new Date + ms; + if (!this.id || time < this.time) { + clearTimeout(this.id); + this.id = setTimeout(this.handler, ms); + this.time = time; + } + }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + { if (array[i] == elt) { return i } } + return -1 + } + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 50; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = {toString: function(){return "CodeMirror.Pass"}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) { nextTab = string.length; } + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + { return pos + Math.min(skipped, goal - col) } + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) { return pos } + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + { spaceStrs.push(lst(spaceStrs) + " "); } + return spaceStrs[n] + } + + function lst(arr) { return arr[arr.length-1] } + + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } + return out + } + + function insertSorted(array, value, score) { + var pos = 0, priority = score(value); + while (pos < array.length && score(array[pos]) <= priority) { pos++; } + array.splice(pos, 0, value); + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) { copyObj(props, inst); } + return inst + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordCharBasic(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) + } + function isWordChar(ch, helper) { + if (!helper) { return isWordCharBasic(ch) } + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } + return helper.test(ch) + } + + function isEmpty(obj) { + for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } + return true + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } + + // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. + function skipExtendingChars(str, pos, dir) { + while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } + return pos + } + + // Returns the value from the range [`from`; `to`] that satisfies + // `pred` and is closest to `from`. Assumes that at least `to` + // satisfies `pred`. Supports `from` being greater than `to`. + function findFirst(pred, from, to) { + // At any point we are certain `to` satisfies `pred`, don't know + // whether `from` does. + var dir = from > to ? -1 : 1; + for (;;) { + if (from == to) { return from } + var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); + if (mid == from) { return pred(mid) ? from : to } + if (pred(mid)) { to = mid; } + else { from = mid + dir; } + } + } + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) { return f(from, to, "ltr", 0) } + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); + found = true; + } + } + if (!found) { f(from, to, "ltr"); } + } + + var bidiOther = null; + function getBidiPartAt(order, ch, sticky) { + var found; + bidiOther = null; + for (var i = 0; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < ch && cur.to > ch) { return i } + if (cur.to == ch) { + if (cur.from != cur.to && sticky == "before") { found = i; } + else { bidiOther = i; } + } + if (cur.from == ch) { + if (cur.from != cur.to && sticky != "before") { found = i; } + else { bidiOther = i; } + } + } + return found != null ? found : bidiOther + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6f9 + var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; + function charType(code) { + if (code <= 0xf7) { return lowTypes.charAt(code) } + else if (0x590 <= code && code <= 0x5f4) { return "R" } + else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } + else if (0x6ee <= code && code <= 0x8ac) { return "r" } + else if (0x2000 <= code && code <= 0x200b) { return "w" } + else if (code == 0x200c) { return "b" } + else { return "L" } + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str, direction) { + var outerType = direction == "ltr" ? "L" : "R"; + + if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } + var len = str.length, types = []; + for (var i = 0; i < len; ++i) + { types.push(charType(str.charCodeAt(i))); } + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { + var type = types[i$1]; + if (type == "m") { types[i$1] = prev; } + else { prev = type; } + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { + var type$1 = types[i$2]; + if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } + else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { + var type$2 = types[i$3]; + if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } + else if (type$2 == "," && prev$1 == types[i$3+1] && + (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } + prev$1 = type$2; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i$4 = 0; i$4 < len; ++i$4) { + var type$3 = types[i$4]; + if (type$3 == ",") { types[i$4] = "N"; } + else if (type$3 == "%") { + var end = (void 0); + for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i$4; j < end; ++j) { types[j] = replace; } + i$4 = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { + var type$4 = types[i$5]; + if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } + else if (isStrong.test(type$4)) { cur$1 = type$4; } + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i$6 = 0; i$6 < len; ++i$6) { + if (isNeutral.test(types[i$6])) { + var end$1 = (void 0); + for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} + var before = (i$6 ? types[i$6-1] : outerType) == "L"; + var after = (end$1 < len ? types[end$1] : outerType) == "L"; + var replace$1 = before == after ? (before ? "L" : "R") : outerType; + for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } + i$6 = end$1 - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i$7 = 0; i$7 < len;) { + if (countsAsLeft.test(types[i$7])) { + var start = i$7; + for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} + order.push(new BidiSpan(0, start, i$7)); + } else { + var pos = i$7, at = order.length, isRTL = direction == "rtl" ? 1 : 0; + for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} + for (var j$2 = pos; j$2 < i$7;) { + if (countsAsNum.test(types[j$2])) { + if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); at += isRTL; } + var nstart = j$2; + for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} + order.splice(at, 0, new BidiSpan(2, nstart, j$2)); + at += isRTL; + pos = j$2; + } else { ++j$2; } + } + if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } + } + } + if (direction == "ltr") { + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + } + + return direction == "rtl" ? order.reverse() : order + } + })(); + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line, direction) { + var order = line.order; + if (order == null) { order = line.order = bidiOrdering(line.text, direction); } + return order + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var noHandlers = []; + + var on = function(emitter, type, f) { + if (emitter.addEventListener) { + emitter.addEventListener(type, f, false); + } else if (emitter.attachEvent) { + emitter.attachEvent("on" + type, f); + } else { + var map = emitter._handlers || (emitter._handlers = {}); + map[type] = (map[type] || noHandlers).concat(f); + } + }; + + function getHandlers(emitter, type) { + return emitter._handlers && emitter._handlers[type] || noHandlers + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) { + emitter.removeEventListener(type, f, false); + } else if (emitter.detachEvent) { + emitter.detachEvent("on" + type, f); + } else { + var map = emitter._handlers, arr = map && map[type]; + if (arr) { + var index = indexOf(arr, f); + if (index > -1) + { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } + } + } + } + + function signal(emitter, type /*, values...*/) { + var handlers = getHandlers(emitter, type); + if (!handlers.length) { return } + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) { return } + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) + { set.push(arr[i]); } } + } + + function hasHandler(emitter, type) { + return getHandlers(emitter, type).length > 0 + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + function e_preventDefault(e) { + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } + } + function e_stopPropagation(e) { + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + + function e_target(e) {return e.target || e.srcElement} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) { b = 1; } + else if (e.button & 2) { b = 3; } + else if (e.button & 4) { b = 2; } + } + if (mac && e.ctrlKey && b == 1) { b = 3; } + return b + } + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) { return false } + var div = elt('div'); + return "draggable" in div || "dragDrop" in div + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) { return badBidiRects } + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + var r1 = range(txt, 1, 2).getBoundingClientRect(); + removeChildren(measure); + if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) + return badBidiRects = (r1.right - r0.right < 3) + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) { nl = string.length; } + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result + } : function (string) { return string.split(/\r\n?|\n/); }; + + var hasSelection = window.getSelection ? function (te) { + try { return te.selectionStart != te.selectionEnd } + catch(e) { return false } + } : function (te) { + var range; + try {range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) { return false } + return range.compareEndPoints("StartToEnd", range) != 0 + }; + + var hasCopyEvent = (function () { + var e = elt("div"); + if ("oncopy" in e) { return true } + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function" + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) { return badZoomedRects } + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 + } + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + function defineMode(name, mode) { + if (arguments.length > 2) + { mode.dependencies = Array.prototype.slice.call(arguments, 2); } + modes[name] = mode; + } + + function defineMIME(mime, spec) { + mimeModes[mime] = spec; + } + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + function resolveMode(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") { found = {name: found}; } + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return resolveMode("application/xml") + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { + return resolveMode("application/json") + } + if (typeof spec == "string") { return {name: spec} } + else { return spec || {name: "null"} } + } + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + function getMode(options, spec) { + spec = resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) { return getMode(options, "text/plain") } + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) { continue } + if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) { modeObj.helperType = spec.helperType; } + if (spec.modeProps) { for (var prop$1 in spec.modeProps) + { modeObj[prop$1] = spec.modeProps[prop$1]; } } + + return modeObj + } + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = {}; + function extendMode(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + } + + function copyState(mode, state) { + if (state === true) { return state } + if (mode.copyState) { return mode.copyState(state) } + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) { val = val.concat([]); } + nstate[n] = val; + } + return nstate + } + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + function innerMode(mode, state) { + var info; + while (mode.innerMode) { + info = mode.innerMode(state); + if (!info || info.mode == mode) { break } + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state} + } + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true + } + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = function(string, tabSize, lineOracle) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + this.lineOracle = lineOracle; + }; + + StringStream.prototype.eol = function () {return this.pos >= this.string.length}; + StringStream.prototype.sol = function () {return this.pos == this.lineStart}; + StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; + StringStream.prototype.next = function () { + if (this.pos < this.string.length) + { return this.string.charAt(this.pos++) } + }; + StringStream.prototype.eat = function (match) { + var ch = this.string.charAt(this.pos); + var ok; + if (typeof match == "string") { ok = ch == match; } + else { ok = ch && (match.test ? match.test(ch) : match(ch)); } + if (ok) {++this.pos; return ch} + }; + StringStream.prototype.eatWhile = function (match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start + }; + StringStream.prototype.eatSpace = function () { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this.pos; } + return this.pos > start + }; + StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; + StringStream.prototype.skipTo = function (ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true} + }; + StringStream.prototype.backUp = function (n) {this.pos -= n;}; + StringStream.prototype.column = function () { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.indentation = function () { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.match = function (pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) { this.pos += pattern.length; } + return true + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) { return null } + if (match && consume !== false) { this.pos += match[0].length; } + return match + } + }; + StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; + StringStream.prototype.hideFirstChars = function (n, inner) { + this.lineStart += n; + try { return inner() } + finally { this.lineStart -= n; } + }; + StringStream.prototype.lookAhead = function (n) { + var oracle = this.lineOracle; + return oracle && oracle.lookAhead(n) + }; + StringStream.prototype.baseToken = function () { + var oracle = this.lineOracle; + return oracle && oracle.baseToken(this.pos) + }; + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } + var chunk = doc; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break } + n -= sz; + } + } + return chunk.lines[n] + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function (line) { + var text = line.text; + if (n == end.line) { text = text.slice(0, end.ch); } + if (n == start.line) { text = text.slice(start.ch); } + out.push(text); + ++n; + }); + return out + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value + return out + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) { return null } + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) { break } + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { + var child = chunk.children[i$1], ch = child.height; + if (h < ch) { chunk = child; continue outer } + h -= ch; + n += child.chunkSize(); + } + return n + } while (!chunk.lines) + var i = 0; + for (; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) { break } + h -= lh; + } + return n + i + } + + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)) + } + + // A Pos instance represents a position within the text. + function Pos(line, ch, sticky) { + if ( sticky === void 0 ) sticky = null; + + if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } + this.line = line; + this.ch = ch; + this.sticky = sticky; + } + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + function cmp(a, b) { return a.line - b.line || a.ch - b.ch } + + function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } + + function copyPos(x) {return Pos(x.line, x.ch)} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} + function clipPos(doc, pos) { + if (pos.line < doc.first) { return Pos(doc.first, 0) } + var last = doc.first + doc.size - 1; + if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } + return clipToLen(pos, getLine(doc, pos.line).text.length) + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } + else if (ch < 0) { return Pos(pos.line, 0) } + else { return pos } + } + function clipPosArray(doc, array) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } + return out + } + + var SavedContext = function(state, lookAhead) { + this.state = state; + this.lookAhead = lookAhead; + }; + + var Context = function(doc, state, line, lookAhead) { + this.state = state; + this.doc = doc; + this.line = line; + this.maxLookAhead = lookAhead || 0; + this.baseTokens = null; + this.baseTokenPos = 1; + }; + + Context.prototype.lookAhead = function (n) { + var line = this.doc.getLine(this.line + n); + if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } + return line + }; + + Context.prototype.baseToken = function (n) { + if (!this.baseTokens) { return null } + while (this.baseTokens[this.baseTokenPos] <= n) + { this.baseTokenPos += 2; } + var type = this.baseTokens[this.baseTokenPos + 1]; + return {type: type && type.replace(/( |^)overlay .*/, ""), + size: this.baseTokens[this.baseTokenPos] - n} + }; + + Context.prototype.nextLine = function () { + this.line++; + if (this.maxLookAhead > 0) { this.maxLookAhead--; } + }; + + Context.fromSaved = function (doc, saved, line) { + if (saved instanceof SavedContext) + { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } + else + { return new Context(doc, copyState(doc.mode, saved), line) } + }; + + Context.prototype.save = function (copy) { + var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; + return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state + }; + + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, context, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, + lineClasses, forceToEnd); + var state = context.state; + + // Run overlays, adjust style array. + var loop = function ( o ) { + context.baseTokens = st; + var overlay = cm.state.overlays[o], i = 1, at = 0; + context.state = true; + runMode(cm, line.text, overlay.mode, context, function (end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + { st.splice(i, 1, end, st[i+1], i_end); } + i += 2; + at = Math.min(end, i_end); + } + if (!style) { return } + if (overlay.opaque) { + st.splice(start, i - start, end, "overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "overlay " + style; + } + } + }, lineClasses); + context.state = state; + context.baseTokens = null; + context.baseTokenPos = 1; + }; + + for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var context = getContextBefore(cm, lineNo(line)); + var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); + var result = highlightLine(cm, line, context); + if (resetState) { context.state = resetState; } + line.stateAfter = context.save(!resetState); + line.styles = result.styles; + if (result.classes) { line.styleClasses = result.classes; } + else if (line.styleClasses) { line.styleClasses = null; } + if (updateFrontier === cm.doc.highlightFrontier) + { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } + } + return line.styles + } + + function getContextBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) { return new Context(doc, true, n) } + var start = findStartLine(cm, n, precise); + var saved = start > doc.first && getLine(doc, start - 1).stateAfter; + var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); + + doc.iter(start, n, function (line) { + processLine(cm, line.text, context); + var pos = context.line; + line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; + context.nextLine(); + }); + if (precise) { doc.modeFrontier = context.line; } + return context + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, context, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize, context); + stream.start = stream.pos = startAt || 0; + if (text == "") { callBlankLine(mode, context.state); } + while (!stream.eol()) { + readToken(mode, stream, context.state); + stream.start = stream.pos; + } + } + + function callBlankLine(mode, state) { + if (mode.blankLine) { return mode.blankLine(state) } + if (!mode.innerMode) { return } + var inner = innerMode(mode, state); + if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) { inner[0] = innerMode(mode, state).mode; } + var style = mode.token(stream, state); + if (stream.pos > stream.start) { return style } + } + throw new Error("Mode " + mode.name + " failed to advance stream.") + } + + var Token = function(stream, type, state) { + this.start = stream.start; this.end = stream.pos; + this.string = stream.current(); + this.type = type || null; + this.state = state; + }; + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; + if (asArray) { tokens = []; } + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, context.state); + if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } + } + return asArray ? tokens : new Token(stream, style, context.state) + } + + function extractLineClasses(type, output) { + if (type) { for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) { break } + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + { output[prop] = lineClass[2]; } + else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop])) + { output[prop] += " " + lineClass[2]; } + } } + return type + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize, context), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) { processLine(cm, text, context, stream.pos); } + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) { style = "m-" + (style ? mName + " " + style : mName); } + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 5000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 + // characters, and returns inaccurate measurements in nodes + // starting around 5000 chars. + var pos = Math.min(stream.pos, curStart + 5000); + f(pos, curStyle); + curStart = pos; + } + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) { return doc.first } + var line = getLine(doc, search - 1), after = line.stateAfter; + if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) + { return search } + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline + } + + function retreatFrontier(doc, n) { + doc.modeFrontier = Math.min(doc.modeFrontier, n); + if (doc.highlightFrontier < n - 10) { return } + var start = doc.first; + for (var line = n - 1; line > start; line--) { + var saved = getLine(doc, line).stateAfter; + // change is on 3 + // state on line 1 looked ahead 2 -- so saw 3 + // test 1 + 2 < 3 should cover this + if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { + start = line + 1; + break + } + } + doc.highlightFrontier = Math.min(doc.highlightFrontier, start); + } + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + function seeReadOnlySpans() { + sawReadOnlySpans = true; + } + + function seeCollapsedSpans() { + sawCollapsedSpans = true; + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) { return span } + } } + } + + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + var r; + for (var i = 0; i < spans.length; ++i) + { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } + return r + } + + // Add a span to a line. + function addMarkedSpan(line, span, op) { + var inThisOp = op && window.WeakSet && (op.markedSpans || (op.markedSpans = new WeakSet)); + if (inThisOp && inThisOp.has(line.markedSpans)) { + line.markedSpans.push(span); + } else { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + if (inThisOp) { inThisOp.add(line.markedSpans); } + } + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } } + return nw + } + function markedSpansAfter(old, endCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } } + return nw + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) { return null } + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) { return null } + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) { span.to = startCh; } + else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i$1 = 0; i$1 < last.length; ++i$1) { + var span$1 = last[i$1]; + if (span$1.to != null) { span$1.to += offset; } + if (span$1.from == null) { + var found$1 = getMarkedSpanFor(first, span$1.marker); + if (!found$1) { + span$1.from = offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } else { + span$1.from += offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } + } + // Make sure we didn't create any zero-length spans + if (first) { first = clearEmptySpans(first); } + if (last && last != first) { last = clearEmptySpans(last); } + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + { for (var i$2 = 0; i$2 < first.length; ++i$2) + { if (first[i$2].to == null) + { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } + for (var i$3 = 0; i$3 < gap; ++i$3) + { newMarkers.push(gapMarkers); } + newMarkers.push(last); + } + return newMarkers + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + { spans.splice(i--, 1); } + } + if (!spans.length) { return null } + return spans + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function (line) { + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + { (markers || (markers = [])).push(mark); } + } } + }); + if (!markers) { return null } + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + { newParts.push({from: p.from, to: m.from}); } + if (dto > 0 || !mk.inclusiveRight && !dto) + { newParts.push({from: m.to, to: p.to}); } + parts.splice.apply(parts, newParts); + j += newParts.length - 3; + } + } + return parts + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.detachLine(line); } + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.attachLine(line); } + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) { return lenDiff } + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) { return -fromCmp } + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) { return toCmp } + return b.id - a.id + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + { found = sp.marker; } + } } + return found + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } + + function collapsedSpanAround(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } + } } + return found + } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) { continue } + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } + if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || + fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) + { return true } + } } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + { line = merged.find(-1, true).line; } + return line + } + + function visualLineEnd(line) { + var merged; + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return line + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line + ;(lines || (lines = [])).push(line); + } + return lines + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) { return lineN } + return lineNo(vis) + } + + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) { return lineN } + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) { return lineN } + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return lineNo(line) + 1 + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) { continue } + if (sp.from == null) { return true } + if (sp.marker.widgetNode) { continue } + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + { return true } + } } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) + } + if (span.marker.inclusiveRight && span.to == line.text.length) + { return true } + for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) { return true } + } + } + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) { break } + else { h += line.height; } + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i$1 = 0; i$1 < p.children.length; ++i$1) { + var cur = p.children[i$1]; + if (cur == chunk) { break } + else { h += cur.height; } + } + } + return h + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) { return 0 } + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found$1 = merged.find(0, true); + len -= cur.text.length - found$1.from.ch; + cur = found$1.to.line; + len += cur.text.length - found$1.to.ch; + } + return len + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function (line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + + Line.prototype.lineNo = function () { return lineNo(this) }; + eventMixin(Line); + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + if (line.order != null) { line.order = null; } + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) { return null } + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")) + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, + col: 0, pos: 0, cm: cm, + trailingSpace: false, + splitSpaces: cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) + { builder.addToken = buildTokenBadBidi(builder.addToken, order); } + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } + if (line.styleClasses.textClass) + { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) + ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit) { + var last = builder.content.lastChild; + if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) + { builder.content.className = "cm-tab-wrap-hack"; } + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } + + return builder + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, css, attributes) { + if (!text) { return } + var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + var content; + if (!special.test(text)) { + builder.col += text.length; + content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) { mustWrap = true; } + builder.pos += text.length; + } else { + content = document.createDocumentFragment(); + var pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } + else { content.appendChild(txt); } + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) { break } + pos += skipped + 1; + var txt$1 = (void 0); + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt$1.setAttribute("role", "presentation"); + txt$1.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt$1.setAttribute("cm-text", m[0]); + builder.col += 1; + } else { + txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); + txt$1.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } + else { content.appendChild(txt$1); } + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt$1); + builder.pos++; + } + } + builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; + if (style || startStyle || endStyle || mustWrap || css || attributes) { + var fullStyle = style || ""; + if (startStyle) { fullStyle += startStyle; } + if (endStyle) { fullStyle += endStyle; } + var token = elt("span", [content], fullStyle, css); + if (attributes) { + for (var attr in attributes) { if (attributes.hasOwnProperty(attr) && attr != "style" && attr != "class") + { token.setAttribute(attr, attributes[attr]); } } + } + return builder.content.appendChild(token) + } + builder.content.appendChild(content); + } + + // Change some spaces to NBSP to prevent the browser from collapsing + // trailing spaces at the end of a line when rendering text (issue #1362). + function splitSpaces(text, trailingBefore) { + if (text.length > 1 && !/ /.test(text)) { return text } + var spaceBefore = trailingBefore, result = ""; + for (var i = 0; i < text.length; i++) { + var ch = text.charAt(i); + if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) + { ch = "\u00a0"; } + result += ch; + spaceBefore = ch == " "; + } + return result + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function (builder, text, style, startStyle, endStyle, css, attributes) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + var part = (void 0); + for (var i = 0; i < order.length; i++) { + part = order[i]; + if (part.to > start && part.from <= start) { break } + } + if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, css, attributes) } + inner(builder, text.slice(0, part.to - start), style, startStyle, null, css, attributes); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + } + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + { widget = builder.content.appendChild(document.createElement("span")); } + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + builder.trailingSpace = false; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i$1 = 1; i$1 < styles.length; i$1+=2) + { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } + return + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed, attributes; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = css = ""; + attributes = null; + collapsed = null; nextChange = Infinity; + var foundBookmarks = [], endStyles = (void 0); + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) { spanStyle += " " + m.className; } + if (m.css) { css = (css ? css + ";" : "") + m.css; } + if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } + if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } + // support for the old title property + // https://github.com/codemirror/CodeMirror/pull/5673 + if (m.title) { (attributes || (attributes = {})).title = m.title; } + if (m.attributes) { + for (var attr in m.attributes) + { (attributes || (attributes = {}))[attr] = m.attributes[attr]; } + } + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + { collapsed = sp; } + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) + { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } + + if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) + { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) { return } + if (collapsed.to == pos) { collapsed = false; } + } + } + if (pos >= len) { break } + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", css, attributes); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array + } + + var operationGroup = null; + + function pushOperation(op) { + if (operationGroup) { + operationGroup.ops.push(op); + } else { + op.ownsGroup = operationGroup = { + ops: [op], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + { callbacks[i].call(null); } + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } + } + } while (i < callbacks.length) + } + + function finishOperation(op, endCb) { + var group = op.ownsGroup; + if (!group) { return } + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + endCb(group); + } + } + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = getHandlers(emitter, type); + if (!arr.length) { return } + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + var loop = function ( i ) { + list.push(function () { return arr[i].apply(null, args); }); + }; + + for (var i = 0; i < arr.length; ++i) + loop( i ); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) { delayed[i](); } + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") { updateLineText(cm, lineView); } + else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } + else if (type == "class") { updateLineClasses(cm, lineView); } + else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } + } + return lineView.node + } + + function updateLineBackground(cm, lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) { cls += " CodeMirror-linebackground"; } + if (lineView.background) { + if (cls) { lineView.background.className = cls; } + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + cm.display.input.setUneditable(lineView.background); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built + } + return buildLineContent(cm, lineView) + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) { lineView.node = built.pre; } + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(cm, lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(cm, lineView) { + updateLineBackground(cm, lineView); + if (lineView.line.wrapClass) + { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } + else if (lineView.node != lineView.text) + { lineView.node.className = ""; } + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(lineView.gutterBackground); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap$1 = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); + gutterWrap.setAttribute("aria-hidden", "true"); + cm.display.input.setUneditable(gutterWrap); + wrap$1.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + { gutterWrap.className += " " + lineView.line.gutterClass; } + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + { lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } + if (markers) { for (var k = 0; k < cm.display.gutterSpecs.length; ++k) { + var id = cm.display.gutterSpecs[k].className, found = markers.hasOwnProperty(id) && markers[id]; + if (found) + { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } + } } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) { lineView.alignable = null; } + var isWidget = classTest("CodeMirror-linewidget"); + for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { + next = node.nextSibling; + if (isWidget.test(node.className)) { lineView.node.removeChild(node); } + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) { lineView.bgClass = built.bgClass; } + if (built.textClass) { lineView.textClass = built.textClass; } + + updateLineClasses(cm, lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) { return } + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget" + (widget.className ? " " + widget.className : "")); + if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + { wrap.insertBefore(node, lineView.gutter || lineView.text); } + else + { wrap.appendChild(node); } + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } + } + } + + function widgetHeight(widget) { + if (widget.height != null) { return widget.height } + var cm = widget.doc.cm; + if (!cm) { return 0 } + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } + if (widget.noHScroll) + { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.parentNode.offsetHeight + } + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + { return true } + } + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} + function paddingH(display) { + if (display.cachedPaddingH) { return display.cachedPaddingH } + var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } + return data + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + { heights.push((cur.bottom + next.top) / 2 - rect.top); } + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + { return {map: lineView.measure.map, cache: lineView.measure.cache} } + if (lineView.rest) { + for (var i = 0; i < lineView.rest.length; i++) + { if (lineView.rest[i] == line) + { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } + for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) + { if (lineNo(lineView.rest[i$1]) > lineN) + { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + } + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + { return cm.display.view[findViewIndex(cm, lineN)] } + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + { return ext } + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) { + view = null; + } else if (view && view.changes) { + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } + if (!view) + { view = updateExternalMeasurement(cm, line); } + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + } + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) { ch = -1; } + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + { prepared.rect = prepared.view.text.getBoundingClientRect(); } + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) { prepared.cache[key] = found; } + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom} + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse, mStart, mEnd; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + mStart = map[i]; + mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) { collapse = "right"; } + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + { collapse = bias; } + if (bias == "left" && start == 0) + { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } } + if (bias == "right" && start == mEnd - mStart) + { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } } + break + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} + } + + function getUsefulRect(rects, bias) { + var rect = nullRect; + if (bias == "left") { for (var i = 0; i < rects.length; i++) { + if ((rect = rects[i]).left != rect.right) { break } + } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { + if ((rect = rects[i$1]).left != rect.right) { break } + } } + return rect + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) + { rect = node.parentNode.getBoundingClientRect(); } + else + { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } + if (rect.left || rect.right || start == 0) { break } + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) { collapse = bias = "right"; } + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + { rect = rects[bias == "right" ? rects.length - 1 : 0]; } + else + { rect = node.getBoundingClientRect(); } + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } + else + { rect = nullRect; } + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + var i = 0; + for (; i < heights.length - 1; i++) + { if (mid < heights[i]) { break } } + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) { result.bogus = true; } + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + { return rect } + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY} + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { lineView.measure.caches[i] = {}; } } + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + { clearLineMeasurementCacheFor(cm.display.view[i]); } + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } + cm.display.lineNumChars = null; + } + + function pageScrollX() { + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 + // which causes page_Offset and bounding client rects to use + // different reference viewports and invalidate our calculations. + if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } + return window.pageXOffset || (document.documentElement || document.body).scrollLeft + } + function pageScrollY() { + if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } + return window.pageYOffset || (document.documentElement || document.body).scrollTop + } + + function widgetTopHeight(lineObj) { + var ref = visualLine(lineObj); + var widgets = ref.widgets; + var height = 0; + if (widgets) { for (var i = 0; i < widgets.length; ++i) { if (widgets[i].above) + { height += widgetHeight(widgets[i]); } } } + return height + } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"./null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { + if (!includeWidgets) { + var height = widgetTopHeight(lineObj); + rect.top += height; rect.bottom += height; + } + if (context == "line") { return rect } + if (!context) { context = "local"; } + var yOff = heightAtLine(lineObj); + if (context == "local") { yOff += paddingTop(cm.display); } + else { yOff -= cm.display.viewOffset; } + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"./null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") { return coords } + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` + // and after `char - 1` in writing order of `char - 1` + // A cursor Pos(line, char, "after") is on the same visual line as `char` + // and before `char` in writing order of `char` + // Examples (upper-case letters are RTL, lower-case are LTR): + // Pos(0, 1, ...) + // before after + // ab a|b a|b + // aB a|B aB| + // Ab |Ab A|b + // AB B|A B|A + // Every position after the last character on a line is considered to stick + // to the last character on the line. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) { m.left = m.right; } else { m.right = m.left; } + return intoCoordSystem(cm, lineObj, m, context) + } + var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; + if (ch >= lineObj.text.length) { + ch = lineObj.text.length; + sticky = "before"; + } else if (ch <= 0) { + ch = 0; + sticky = "after"; + } + if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } + + function getBidi(ch, partPos, invert) { + var part = order[partPos], right = part.level == 1; + return get(invert ? ch - 1 : ch, right != invert) + } + var partPos = getBidiPartAt(order, ch, sticky); + var other = bidiOther; + var val = getBidi(ch, partPos, sticky == "before"); + if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } + return val + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0; + pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height} + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, sticky, outside, xRel) { + var pos = Pos(line, ch, sticky); + pos.xRel = xRel; + if (outside) { pos.outside = outside; } + return pos + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } + if (x < 0) { x = 0; } + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); + if (!collapsed) { return found } + var rangeEnd = collapsed.find(1); + if (rangeEnd.line == lineN) { return rangeEnd } + lineObj = getLine(doc, lineN = rangeEnd.line); + } + } + + function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { + y -= widgetTopHeight(lineObj); + var end = lineObj.text.length; + var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); + end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); + return {begin: begin, end: end} + } + + function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; + return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) + } + + // Returns true if the given side of a box is after the given + // coordinates, in top-to-bottom, left-to-right order. + function boxIsAfter(box, x, y, left) { + return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + // Move y into line-local coordinate space + y -= heightAtLine(lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + // When directly calling `measureCharPrepared`, we have to adjust + // for the widgets at this line. + var widgetHeight = widgetTopHeight(lineObj); + var begin = 0, end = lineObj.text.length, ltr = true; + + var order = getOrder(lineObj, cm.doc.direction); + // If the line isn't plain left-to-right text, first figure out + // which bidi section the coordinates fall into. + if (order) { + var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) + (cm, lineObj, lineNo, preparedMeasure, order, x, y); + ltr = part.level != 1; + // The awkward -1 offsets are needed because findFirst (called + // on these below) will treat its first bound as inclusive, + // second as exclusive, but we want to actually address the + // characters in the part's range + begin = ltr ? part.from : part.to - 1; + end = ltr ? part.to : part.from - 1; + } + + // A binary search to find the first character whose bounding box + // starts after the coordinates. If we run across any whose box wrap + // the coordinates, store that. + var chAround = null, boxAround = null; + var ch = findFirst(function (ch) { + var box = measureCharPrepared(cm, preparedMeasure, ch); + box.top += widgetHeight; box.bottom += widgetHeight; + if (!boxIsAfter(box, x, y, false)) { return false } + if (box.top <= y && box.left <= x) { + chAround = ch; + boxAround = box; + } + return true + }, begin, end); + + var baseX, sticky, outside = false; + // If a box around the coordinates was found, use that + if (boxAround) { + // Distinguish coordinates nearer to the left or right side of the box + var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; + ch = chAround + (atStart ? 0 : 1); + sticky = atStart ? "after" : "before"; + baseX = atLeft ? boxAround.left : boxAround.right; + } else { + // (Adjust for extended bound, if necessary.) + if (!ltr && (ch == end || ch == begin)) { ch++; } + // To determine which side to associate with, get the box to the + // left of the character and compare it's vertical position to the + // coordinates + sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : + (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? + "after" : "before"; + // Now get accurate coordinates for this place, in order to get a + // base X position + var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure); + baseX = coords.left; + outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; + } + + ch = skipExtendingChars(lineObj.text, ch, 1); + return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) + } + + function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { + // Bidi parts are sorted left-to-right, and in a non-line-wrapping + // situation, we can take this ordering to correspond to the visual + // ordering. This finds the first part whose end is after the given + // coordinates. + var index = findFirst(function (i) { + var part = order[i], ltr = part.level != 1; + return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), + "line", lineObj, preparedMeasure), x, y, true) + }, 0, order.length - 1); + var part = order[index]; + // If this isn't the first part, the part's start is also after + // the coordinates, and the coordinates aren't on the same line as + // that start, move one part back. + if (index > 0) { + var ltr = part.level != 1; + var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), + "line", lineObj, preparedMeasure); + if (boxIsAfter(start, x, y, true) && start.top > y) + { part = order[index - 1]; } + } + return part + } + + function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { + // In a wrapped line, rtl text on wrapping boundaries can do things + // that don't correspond to the ordering in our `order` array at + // all, so a binary search doesn't work, and we want to return a + // part that only spans one line so that the binary search in + // coordsCharInner is safe. As such, we first find the extent of the + // wrapped line, and then do a flat search in which we discard any + // spans that aren't on the line. + var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); + var begin = ref.begin; + var end = ref.end; + if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } + var part = null, closestDist = null; + for (var i = 0; i < order.length; i++) { + var p = order[i]; + if (p.from >= end || p.to <= begin) { continue } + var ltr = p.level != 1; + var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; + // Weigh against spans ending before this, so that they are only + // picked if nothing ends after + var dist = endX < x ? x - endX + 1e9 : endX - x; + if (!part || closestDist > dist) { + part = p; + closestDist = dist; + } + } + if (!part) { part = order[order.length - 1]; } + // Clip the part to the wrapped line. + if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } + if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } + return part + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) { return display.cachedTextHeight } + if (measureText == null) { + measureText = elt("pre", null, "CodeMirror-line-like"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) { display.cachedTextHeight = height; } + removeChildren(display.measure); + return height || 1 + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) { return display.cachedCharWidth } + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor], "CodeMirror-line-like"); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) { display.cachedCharWidth = width; } + return width || 10 + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + var id = cm.display.gutterSpecs[i].className; + left[id] = n.offsetLeft + n.clientLeft + gutterLeft; + width[id] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth} + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function (line) { + if (lineIsHidden(cm.doc, line)) { return 0 } + + var widgetsHeight = 0; + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } + } } + + if (wrapping) + { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } + else + { return widgetsHeight + th } + } + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function (line) { + var estHeight = est(line); + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + }); + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e$1) { return null } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel > 0 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) { return null } + n -= cm.display.viewFrom; + if (n < 0) { return null } + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) { return i } + } + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) { from = cm.doc.first; } + if (to == null) { to = cm.doc.first + cm.doc.size; } + if (!lendiff) { lendiff = 0; } + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + { display.updateLineNumbers = from; } + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + { resetView(cm); } + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut$1 = viewCuttingPoint(cm, from, from, -1); + if (cut$1) { + display.view = display.view.slice(0, cut$1.index); + display.viewTo = cut$1.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + { ext.lineN += lendiff; } + else if (from < ext.lineN + ext.size) + { display.externalMeasured = null; } + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + { display.externalMeasured = null; } + + if (line < display.viewFrom || line >= display.viewTo) { return } + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) { return } + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) { arr.push(type); } + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + { return {index: index, lineN: newN} } + var n = cm.display.viewFrom; + for (var i = 0; i < index; i++) + { n += view[i].size; } + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) { return null } + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) { return null } + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN} + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } + else if (display.viewFrom < from) + { display.view = display.view.slice(findViewIndex(cm, from)); } + display.viewFrom = from; + if (display.viewTo < to) + { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } + else if (display.viewTo > to) + { display.view = display.view.slice(0, findViewIndex(cm, to)); } + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } + } + return dirty + } + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + if ( primary === void 0 ) primary = true; + + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + var customCursor = cm.options.$customCursor; + if (customCursor) { primary = true; } + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (!primary && i == doc.sel.primIndex) { continue } + var range = doc.sel.ranges[i]; + if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } + var collapsed = range.empty(); + if (customCursor) { + var head = customCursor(cm, range); + if (head) { drawSelectionCursor(cm, head, curFragment); } + } else if (collapsed || cm.options.showCursorWhenSelecting) { + drawSelectionCursor(cm, range.head, curFragment); + } + if (!collapsed) + { drawSelectionRange(cm, range, selFragment); } + } + return result + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) { + var charPos = charCoords(cm, head, "div", null, null); + var width = charPos.right - charPos.left; + cursor.style.width = (width > 0 ? width : cm.defaultCharWidth()) + "px"; + } + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + var docLTR = doc.direction == "ltr"; + + function add(left, top, width, bottom) { + if (top < 0) { top = 0; } + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias) + } + + function wrapX(pos, dir, side) { + var extent = wrappedLineExtentChar(cm, lineObj, null, pos); + var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; + var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); + return coords(ch, prop)[prop] + } + + var order = getOrder(lineObj, doc.direction); + iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { + var ltr = dir == "ltr"; + var fromPos = coords(from, ltr ? "left" : "right"); + var toPos = coords(to - 1, ltr ? "right" : "left"); + + var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; + var first = i == 0, last = !order || i == order.length - 1; + if (toPos.top - fromPos.top <= 3) { // Single line + var openLeft = (docLTR ? openStart : openEnd) && first; + var openRight = (docLTR ? openEnd : openStart) && last; + var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; + var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; + add(left, fromPos.top, right - left, fromPos.bottom); + } else { // Multiple lines + var topLeft, topRight, botLeft, botRight; + if (ltr) { + topLeft = docLTR && openStart && first ? leftSide : fromPos.left; + topRight = docLTR ? rightSide : wrapX(from, dir, "before"); + botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); + botRight = docLTR && openEnd && last ? rightSide : toPos.right; + } else { + topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); + topRight = !docLTR && openStart && first ? rightSide : fromPos.right; + botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; + botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); + } + add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); + if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } + add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); + } + + if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } + if (cmpCoords(toPos, start) < 0) { start = toPos; } + if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } + if (cmpCoords(toPos, end) < 0) { end = toPos; } + }); + return {start: start, end: end} + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + { add(leftSide, leftEnd.bottom, null, rightStart.top); } + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) { return } + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + { display.blinker = setInterval(function () { + if (!cm.hasFocus()) { onBlur(cm); } + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); } + else if (cm.options.cursorBlinkRate < 0) + { display.cursorDiv.style.visibility = "hidden"; } + } + + function ensureFocus(cm) { + if (!cm.hasFocus()) { + cm.display.input.focus(); + if (!cm.state.focused) { onFocus(cm); } + } + } + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function () { if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + if (cm.state.focused) { onBlur(cm); } + } }, 100); + } + + function onFocus(cm, e) { + if (cm.state.delayingBlurEvent && !cm.state.draggingText) { cm.state.delayingBlurEvent = false; } + + if (cm.options.readOnly == "nocursor") { return } + if (!cm.state.focused) { + signal(cm, "focus", cm, e); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm, e) { + if (cm.state.delayingBlurEvent) { return } + + if (cm.state.focused) { + signal(cm, "blur", cm, e); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + var viewTop = Math.max(0, display.scroller.getBoundingClientRect().top); + var oldHeight = display.lineDiv.getBoundingClientRect().top; + var mustScroll = 0; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], wrapping = cm.options.lineWrapping; + var height = (void 0), width = 0; + if (cur.hidden) { continue } + oldHeight += cur.line.height; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + // Check that lines don't extend past the right of the current + // editor width + if (!wrapping && cur.text.firstChild) + { width = cur.text.firstChild.getBoundingClientRect().right - box.left - 1; } + } + var diff = cur.line.height - height; + if (diff > .005 || diff < -.005) { + if (oldHeight < viewTop) { mustScroll -= diff; } + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) + { updateWidgetHeight(cur.rest[j]); } } + } + if (width > cm.display.sizerWidth) { + var chWidth = Math.ceil(width / charWidth(cm.display)); + if (chWidth > cm.display.maxLineLength) { + cm.display.maxLineLength = chWidth; + cm.display.maxLine = cur.line; + cm.display.maxLineChanged = true; + } + } + } + if (Math.abs(mustScroll) > 2) { display.scroller.scrollTop += mustScroll; } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i], parent = w.node.parentNode; + if (parent) { w.height = parent.offsetHeight; } + } } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)} + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, rect) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (rect.top + box.top < 0) { doScroll = true; } + else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) { margin = 0; } + var rect; + if (!cm.options.lineWrapping && pos == end) { + // Set pos and end to the cursor positions around the character pos sticks to + // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch + // If pos == Pos(_, 0, "before"), pos and end are unchanged + end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; + pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; + } + for (var limit = 0; limit < 5; limit++) { + var changed = false; + var coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + rect = {left: Math.min(coords.left, endCoords.left), + top: Math.min(coords.top, endCoords.top) - margin, + right: Math.max(coords.left, endCoords.left), + bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; + var scrollPos = calculateScrollPos(cm, rect); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + updateScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } + } + if (!changed) { break } + } + return rect + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, rect) { + var scrollPos = calculateScrollPos(cm, rect); + if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } + if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, rect) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (rect.top < 0) { rect.top = 0; } + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } + var docBottom = cm.doc.height + paddingVert(display); + var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; + if (rect.top < screentop) { + result.scrollTop = atTop ? 0 : rect.top; + } else if (rect.bottom > screentop + screen) { + var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); + if (newTop != screentop) { result.scrollTop = newTop; } + } + + var gutterSpace = cm.options.fixedGutter ? 0 : display.gutters.offsetWidth; + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft - gutterSpace; + var screenw = displayWidth(cm) - display.gutters.offsetWidth; + var tooWide = rect.right - rect.left > screenw; + if (tooWide) { rect.right = rect.left + screenw; } + if (rect.left < 10) + { result.scrollLeft = 0; } + else if (rect.left < screenleft) + { result.scrollLeft = Math.max(0, rect.left + gutterSpace - (tooWide ? 0 : 10)); } + else if (rect.right > screenw + screenleft - 3) + { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } + return result + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollTop(cm, top) { + if (top == null) { return } + resolveScrollToPos(cm); + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(); + cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; + } + + function scrollToCoords(cm, x, y) { + if (x != null || y != null) { resolveScrollToPos(cm); } + if (x != null) { cm.curOp.scrollLeft = x; } + if (y != null) { cm.curOp.scrollTop = y; } + } + + function scrollToRange(cm, range) { + resolveScrollToPos(cm); + cm.curOp.scrollToPos = range; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + scrollToCoordsRange(cm, from, to, range.margin); + } + } + + function scrollToCoordsRange(cm, from, to, margin) { + var sPos = calculateScrollPos(cm, { + left: Math.min(from.left, to.left), + top: Math.min(from.top, to.top) - margin, + right: Math.max(from.right, to.right), + bottom: Math.max(from.bottom, to.bottom) + margin + }); + scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); + } + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function updateScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) { return } + if (!gecko) { updateDisplaySimple(cm, {top: val}); } + setScrollTop(cm, val, true); + if (gecko) { updateDisplaySimple(cm); } + startWorker(cm, 100); + } + + function setScrollTop(cm, val, forceScroll) { + val = Math.max(0, Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)); + if (cm.display.scroller.scrollTop == val && !forceScroll) { return } + cm.doc.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } + } + + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller, forceScroll) { + val = Math.max(0, Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)); + if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } + cm.display.scrollbars.setScrollLeft(val); + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + } + } + + var NativeScrollbars = function(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + vert.tabIndex = horiz.tabIndex = -1; + place(vert); place(horiz); + + on(vert, "scroll", function () { + if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } + }); + on(horiz, "scroll", function () { + if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } + }); + + this.checkedZeroWidth = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } + }; + + NativeScrollbars.prototype.update = function (measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.scrollTop = 0; + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) { this.zeroWidthHack(); } + this.checkedZeroWidth = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} + }; + + NativeScrollbars.prototype.setScrollLeft = function (pos) { + if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } + if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } + }; + + NativeScrollbars.prototype.setScrollTop = function (pos) { + if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } + if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } + }; + + NativeScrollbars.prototype.zeroWidthHack = function () { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }; + + NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // right corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) + : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); + if (elt != bar) { bar.style.pointerEvents = "none"; } + else { delay.set(1000, maybeDisable); } + } + delay.set(1000, maybeDisable); + }; + + NativeScrollbars.prototype.clear = function () { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + }; + + var NullScrollbars = function () {}; + + NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; + NullScrollbars.prototype.setScrollLeft = function () {}; + NullScrollbars.prototype.setScrollTop = function () {}; + NullScrollbars.prototype.clear = function () {}; + + function updateScrollbars(cm, measure) { + if (!measure) { measure = measureForScrollbars(cm); } + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + { updateHeightsInViewport(cm); } + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else { d.scrollbarFiller.style.display = ""; } + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else { d.gutterFiller.style.display = ""; } + } + + var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function () { + if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } + }); + node.setAttribute("cm-not-content", "true"); + }, function (pos, axis) { + if (axis == "horizontal") { setScrollLeft(cm, pos); } + else { updateScrollTop(cm, pos); } + }, cm); + if (cm.display.scrollbars.addClass) + { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: 0, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId, // Unique ID + markArrays: null // Used by addMarkedSpan + }; + pushOperation(cm.curOp); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp; + if (op) { finishOperation(op, function (group) { + for (var i = 0; i < group.ops.length; i++) + { group.ops[i].cm.curOp = null; } + endOperations(group); + }); } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + { endOperation_R1(ops[i]); } + for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) + { endOperation_W1(ops[i$1]); } + for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM + { endOperation_R2(ops[i$2]); } + for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) + { endOperation_W2(ops[i$3]); } + for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM + { endOperation_finish(ops[i$4]); } + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) { findMaxLine(cm); } + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) { updateHeightsInViewport(cm); } + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + { op.preparedSelection = display.input.prepareSelection(); } + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } + cm.display.maxLineChanged = false; + } + + var takeFocus = op.focus && op.focus == activeElt(); + if (op.preparedSelection) + { cm.display.input.showSelection(op.preparedSelection, takeFocus); } + if (op.updatedDisplay || op.startHeight != cm.doc.height) + { updateScrollbars(cm, op.barMeasure); } + if (op.updatedDisplay) + { setDocumentHeight(cm, op.barMeasure); } + + if (op.selectionChanged) { restartBlink(cm); } + + if (cm.state.focused && op.updateInput) + { cm.display.input.reset(op.typing); } + if (takeFocus) { ensureFocus(op.cm); } + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + { display.wheelStartX = display.wheelStartY = null; } + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } + + if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + maybeScrollWindow(cm, rect); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) { for (var i = 0; i < hidden.length; ++i) + { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } + if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) + { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } + + if (display.wrapper.offsetHeight) + { doc.scrollTop = cm.display.scroller.scrollTop; } + + // Fire change events, and delayed event handlers + if (op.changeObjs) + { signal(cm, "changes", cm, op.changeObjs); } + if (op.update) + { op.update.finish(); } + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) { return f() } + startOperation(cm); + try { return f() } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) { return f.apply(cm, arguments) } + startOperation(cm); + try { return f.apply(cm, arguments) } + finally { endOperation(cm); } + } + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) { return f.apply(this, arguments) } + startOperation(this); + try { return f.apply(this, arguments) } + finally { endOperation(this); } + } + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) { return f.apply(this, arguments) } + startOperation(cm); + try { return f.apply(this, arguments) } + finally { endOperation(cm); } + } + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.highlightFrontier < cm.display.viewTo) + { cm.state.highlight.set(time, bind(highlightWorker, cm)); } + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.highlightFrontier >= cm.display.viewTo) { return } + var end = +new Date + cm.options.workTime; + var context = getContextBefore(cm, doc.highlightFrontier); + var changedLines = []; + + doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { + if (context.line >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; + var highlighted = highlightLine(cm, line, context, true); + if (resetState) { context.state = resetState; } + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) { line.styleClasses = newCls; } + else if (oldCls) { line.styleClasses = null; } + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } + if (ischange) { changedLines.push(context.line); } + line.stateAfter = context.save(); + context.nextLine(); + } else { + if (line.text.length <= cm.options.maxHighlightLength) + { processLine(cm, line.text, context); } + line.stateAfter = context.line % 5 == 0 ? context.save() : null; + context.nextLine(); + } + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true + } + }); + doc.highlightFrontier = context.line; + doc.modeFrontier = Math.max(doc.modeFrontier, context.line); + if (changedLines.length) { runInOp(cm, function () { + for (var i = 0; i < changedLines.length; i++) + { regLineChange(cm, changedLines[i], "text"); } + }); } + } + + // DISPLAY DRAWING + + var DisplayUpdate = function(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + }; + + DisplayUpdate.prototype.signal = function (emitter, type) { + if (hasHandler(emitter, type)) + { this.events.push(arguments); } + }; + DisplayUpdate.prototype.finish = function () { + for (var i = 0; i < this.events.length; i++) + { signal.apply(null, this.events[i]); } + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + function selectionSnapshot(cm) { + if (cm.hasFocus()) { return null } + var active = activeElt(); + if (!active || !contains(cm.display.lineDiv, active)) { return null } + var result = {activeElt: active}; + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { + result.anchorNode = sel.anchorNode; + result.anchorOffset = sel.anchorOffset; + result.focusNode = sel.focusNode; + result.focusOffset = sel.focusOffset; + } + } + return result + } + + function restoreSelection(snapshot) { + if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } + snapshot.activeElt.focus(); + if (!/^(INPUT|TEXTAREA)$/.test(snapshot.activeElt.nodeName) && + snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { + var sel = window.getSelection(), range = document.createRange(); + range.setEnd(snapshot.anchorNode, snapshot.anchorOffset); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + sel.extend(snapshot.focusNode, snapshot.focusOffset); + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + { return false } + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } + if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + { return false } + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var selSnapshot = selectionSnapshot(cm); + if (toUpdate > 4) { display.lineDiv.style.display = "none"; } + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) { display.lineDiv.style.display = ""; } + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + restoreSelection(selSnapshot); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = display.sizer.style.minHeight = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + { break } + } else if (first) { + update.visible = visibleLines(cm.display, cm.doc, viewport); + } + if (!updateDisplayIfNeeded(cm, update)) { break } + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.force = false; + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.finish(); + } + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + { node.style.display = "none"; } + else + { node.parentNode.removeChild(node); } + return next + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) { cur = rm(cur); } + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) { cur = rm(cur); } + } + + function updateGutterSpace(display) { + var width = display.gutters.offsetWidth; + display.sizer.style.marginLeft = width + "px"; + // Send an event to consumers responding to changes in gutter width. + signalLater(display, "gutterChanged", display); + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; + } + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { + if (cm.options.fixedGutter) { + if (view[i].gutter) + { view[i].gutter.style.left = left; } + if (view[i].gutterBackground) + { view[i].gutterBackground.style.left = left; } + } + var align = view[i].alignable; + if (align) { for (var j = 0; j < align.length; j++) + { align[j].style.left = left; } } + } } + if (cm.options.fixedGutter) + { display.gutters.style.left = (comp + gutterW) + "px"; } + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) { return false } + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm.display); + return true + } + return false + } + + function getGutters(gutters, lineNumbers) { + var result = [], sawLineNumbers = false; + for (var i = 0; i < gutters.length; i++) { + var name = gutters[i], style = null; + if (typeof name != "string") { style = name.style; name = name.className; } + if (name == "CodeMirror-linenumbers") { + if (!lineNumbers) { continue } + else { sawLineNumbers = true; } + } + result.push({className: name, style: style}); + } + if (lineNumbers && !sawLineNumbers) { result.push({className: "CodeMirror-linenumbers", style: null}); } + return result + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function renderGutters(display) { + var gutters = display.gutters, specs = display.gutterSpecs; + removeChildren(gutters); + display.lineGutter = null; + for (var i = 0; i < specs.length; ++i) { + var ref = specs[i]; + var className = ref.className; + var style = ref.style; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + className)); + if (style) { gElt.style.cssText = style; } + if (className == "CodeMirror-linenumbers") { + display.lineGutter = gElt; + gElt.style.width = (display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = specs.length ? "" : "none"; + updateGutterSpace(display); + } + + function updateGutters(cm) { + renderGutters(cm.display); + regChange(cm); + alignHorizontally(cm); + } + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input, options) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = eltP("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [lines], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // This attribute is respected by automatic translation systems such as Google Translate, + // and may also be respected by tools used by human translators. + d.wrapper.setAttribute('translate', 'no'); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } + + if (place) { + if (place.appendChild) { place.appendChild(d.wrapper); } + else { place(d.wrapper); } + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + d.gutterSpecs = getGutters(options.gutters, options.lineNumbers); + renderGutters(d); + + input.init(d); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) { wheelPixelsPerUnit = -.53; } + else if (gecko) { wheelPixelsPerUnit = 15; } + else if (chrome) { wheelPixelsPerUnit = -.7; } + else if (safari) { wheelPixelsPerUnit = -1/3; } + + function wheelEventDelta(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } + else if (dy == null) { dy = e.wheelDelta; } + return {x: dx, y: dy} + } + function wheelEventPixels(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta + } + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + var pixelsPerUnit = wheelPixelsPerUnit; + if (e.deltaMode === 0) { + dx = e.deltaX; + dy = e.deltaY; + pixelsPerUnit = 1; + } + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) { return } + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && pixelsPerUnit != null) { + if (dy && canScrollY) + { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * pixelsPerUnit)); } + setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * pixelsPerUnit)); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + { e_preventDefault(e); } + display.wheelStartX = null; // Abort measurement, if in progress + return + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && pixelsPerUnit != null) { + var pixels = dy * pixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) { top = Math.max(0, top + pixels - 50); } + else { bot = Math.min(cm.doc.height, bot + pixels + 50); } + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20 && e.deltaMode !== 0) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function () { + if (display.wheelStartX == null) { return } + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) { return } + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + var Selection = function(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + }; + + Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; + + Selection.prototype.equals = function (other) { + if (other == this) { return true } + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } + } + return true + }; + + Selection.prototype.deepCopy = function () { + var out = []; + for (var i = 0; i < this.ranges.length; i++) + { out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); } + return new Selection(out, this.primIndex) + }; + + Selection.prototype.somethingSelected = function () { + for (var i = 0; i < this.ranges.length; i++) + { if (!this.ranges[i].empty()) { return true } } + return false + }; + + Selection.prototype.contains = function (pos, end) { + if (!end) { end = pos; } + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + { return i } + } + return -1 + }; + + var Range = function(anchor, head) { + this.anchor = anchor; this.head = head; + }; + + Range.prototype.from = function () { return minPos(this.anchor, this.head) }; + Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; + Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(cm, ranges, primIndex) { + var mayTouch = cm && cm.options.selectionsMayTouch; + var prim = ranges[primIndex]; + ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + var diff = cmp(prev.to(), cur.from()); + if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) { --primIndex; } + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex) + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0) + } + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + function changeEnd(change) { + if (!change.text) { return change.to } + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) + } + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) { return pos } + if (cmp(pos, change.to) <= 0) { return changeEnd(change) } + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } + return Pos(line, ch) + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(doc.cm, out, doc.sel.primIndex) + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + { return Pos(nw.line, pos.ch - old.ch + nw.ch) } + else + { return Pos(nw.line + (pos.line - old.line), pos.ch) } + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex) + } + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function (line) { + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + }); + cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) { regChange(cm); } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore) + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + var result = []; + for (var i = start; i < end; ++i) + { result.push(new Line(text[i], spansFor(i), estimateHeight)); } + return result + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) { doc.remove(from.line, nlines); } + if (added.length) { doc.insert(from.line, added); } + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added$1 = linesFor(1, text.length - 1); + added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added$1); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added$2 = linesFor(1, text.length - 1); + if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } + doc.insert(from.line + 1, added$2); + } + + signalLater(doc, "change", doc, change); + } + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) { continue } + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) { continue } + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) { throw new Error("This document is already in use.") } + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + setDirectionClass(cm); + cm.options.direction = doc.direction; + if (!cm.options.lineWrapping) { findMaxLine(cm); } + cm.options.mode = doc.modeOption; + regChange(cm); + } + + function setDirectionClass(cm) { + (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); + } + + function directionChanged(cm) { + runInOp(cm, function () { + setDirectionClass(cm); + regChange(cm); + }); + } + + function History(prev) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = prev ? prev.undoDepth : Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = prev ? prev.maxGeneration : 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); + return histChange + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) { array.pop(); } + else { break } + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done) + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done) + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done) + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, or are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + var last; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + { pushSelectionToHistory(doc.sel, hist.done); } + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) { hist.done.shift(); } + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) { signal(doc, "historyAdded"); } + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + { hist.done[hist.done.length - 1] = sel; } + else + { pushSelectionToHistory(sel, hist.done); } + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + { clearSelectionEvents(hist.undone); } + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + { dest.push(sel); } + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { + if (line.markedSpans) + { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) { return null } + var out; + for (var i = 0; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } + else if (out) { out.push(spans[i]); } + } + return !out ? spans : out.length ? out : null + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) { return null } + var nw = []; + for (var i = 0; i < change.text.length; ++i) + { nw.push(removeClearedSpans(found[i])); } + return nw + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) { return stretched } + if (!stretched) { return old } + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + { if (oldCur[k].marker == span.marker) { continue spans } } + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + var copy = []; + for (var i = 0; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m = (void 0); + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } } } + } + } + return copy + } + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(range, head, other, extend) { + if (extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head) + } else { + return new Range(other || head, head) + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options, extend) { + if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } + setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + var out = []; + var extend = doc.cm && (doc.cm.display.shift || doc.extend); + for (var i = 0; i < doc.sel.ranges.length; i++) + { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } + var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel, options) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + { this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); } + }, + origin: options && options.origin + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } + if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } + else { return sel } + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + { sel = filterSelectionChange(doc, sel, options); } + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm && doc.cm.getOption("readOnly") != "nocursor") + { ensureCursorVisible(doc.cm); } + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) { return } + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = 1; + doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) { out = sel.ranges.slice(0, i); } + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel + } + + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + + // Determine if we should prevent the cursor being placed to the left/right of an atomic marker + // Historically this was determined using the inclusiveLeft/Right option, but the new way to control it + // is with selectLeft/Right + var preventCursorLeft = ("selectLeft" in m) ? !m.selectLeft : m.inclusiveLeft; + var preventCursorRight = ("selectRight" in m) ? !m.selectRight : m.inclusiveRight; + + if ((sp.from == null || (preventCursorLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (preventCursorRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) { break } + else {--i; continue} + } + } + if (!m.atomic) { continue } + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); + if (dir < 0 ? preventCursorRight : preventCursorLeft) + { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + { return skipAtomicInner(doc, near, pos, dir, mayClear) } + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? preventCursorLeft : preventCursorRight) + { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null + } + } } + return pos + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0) + } + return found + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } + else { return null } + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } + else { return null } + } else { + return new Pos(pos.line, pos.ch + dir) + } + } + + function selectAll(cm) { + cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); + } + + // UPDATING + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function () { return obj.canceled = true; } + }; + if (update) { obj.update = function (from, to, text, origin) { + if (from) { obj.from = clipPos(doc, from); } + if (to) { obj.to = clipPos(doc, to); } + if (text) { obj.text = text; } + if (origin !== undefined) { obj.origin = origin; } + }; } + signal(doc, "beforeChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } + + if (obj.canceled) { + if (doc.cm) { doc.cm.curOp.updateInput = 2; } + return null + } + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } + if (doc.cm.state.suppressEdits) { return } + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) { return } + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + var suppress = doc.cm && doc.cm.state.suppressEdits; + if (suppress && !allowSelectionOnly) { return } + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + var i = 0; + for (; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + { break } + } + if (i == source.length) { return } + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return + } + selAfter = event; + } else if (suppress) { + source.push(event); + return + } else { break } + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + var loop = function ( i ) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return {} + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + }; + + for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { + var returned = loop( i$1 ); + + if ( returned ) return returned.v; + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) { return } + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( + Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch) + ); }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + { regLineChange(doc.cm, l, "gutter"); } + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return + } + if (change.from.line > doc.lastLine()) { return } + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } + if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } + else { updateDoc(doc, change, spans); } + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + + if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) + { doc.cantEdit = false; } + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function (line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + { signalCursorActivity(cm); } + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function (line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } + } + + retreatFrontier(doc, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + { regChange(cm); } + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + { regLineChange(cm, from.line, "text"); } + else + { regChange(cm, from.line, to.line + 1, lendiff); } + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) { signalLater(cm, "change", cm, obj); } + if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + var assign; + + if (!to) { to = from; } + if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } + if (typeof code == "string") { code = doc.splitLines(code); } + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue + } + for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { + var cur = sub.changes[j$1]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } + else { no = lineNo(handle); } + if (no == null) { return null } + if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } + return line + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + var height = 0; + for (var i = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length }, + + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) { lines[i].parent = this; } + }, + + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + { if (op(this.lines[at])) { return true } } + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size }, + + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) { break } + at = 0; + } else { at -= sz; } + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) { this.children[i].collapse(lines); } + }, + + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. + // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. + var remaining = child.lines.length % 25 + 25; + for (var pos = remaining; pos < child.lines.length;) { + var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); + child.height -= leaf.height; + this.children.splice(++i, 0, leaf); + leaf.parent = this; + } + child.lines = child.lines.slice(0, remaining); + this.maybeSpill(); + } + break + } + at -= sz; + } + }, + + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) { return } + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10) + me.parent.maybeSpill(); + }, + + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) { return true } + if ((n -= used) == 0) { break } + at = 0; + } else { at -= sz; } + } + } + }; + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = function(doc, node, options) { + if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) + { this[opt] = options[opt]; } } } + this.doc = doc; + this.node = node; + }; + + LineWidget.prototype.clear = function () { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) { return } + for (var i = 0; i < ws.length; ++i) { if (ws[i] == this) { ws.splice(i--, 1); } } + if (!ws.length) { line.widgets = null; } + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) { + runInOp(cm, function () { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + signalLater(cm, "lineWidgetCleared", cm, this, no); + } + }; + + LineWidget.prototype.changed = function () { + var this$1 = this; + + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) { return } + if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } + if (cm) { + runInOp(cm, function () { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); + }); + } + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + { addToScrollTop(cm, diff); } + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } + changeLine(doc, handle, "widget", function (line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) { widgets.push(widget); } + else { widgets.splice(Math.min(widgets.length, Math.max(0, widget.insertAt)), 0, widget); } + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) { addToScrollTop(cm, widget.height); } + cm.curOp.forceUpdate = true; + } + return true + }); + if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } + return widget + } + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + var TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + + // Clear the marker. + TextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) { startOperation(cm); } + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) { signalLater(this, "clear", found.from, found.to); } + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) { regLineChange(cm, lineNo(line), "text"); } + else if (cm) { + if (span.to != null) { max = lineNo(line); } + if (span.from != null) { min = lineNo(line); } + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + { updateLineHeight(line, textHeight(cm.display)); } + } + if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { + var visual = visualLine(this.lines[i$1]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } } + + if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) { reCheckSelection(cm.doc); } + } + if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } + if (withOp) { endOperation(cm); } + if (this.parent) { this.parent.clear(); } + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function (side, lineObj) { + if (side == null && this.type == "bookmark") { side = 1; } + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) { return from } + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) { return to } + } + } + return from && {from: from, to: to} + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function () { + var this$1 = this; + + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) { return } + runInOp(cm, function () { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + { updateLineHeight(line, line.height + dHeight); } + } + signalLater(cm, "markerChanged", cm, this$1); + }); + }; + + TextMarker.prototype.attachLine = function (line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } + } + this.lines.push(line); + }; + + TextMarker.prototype.detachLine = function (line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp + ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + eventMixin(TextMarker); + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) { return markTextShared(doc, from, to, options, type) } + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) { copyObj(options, marker, false); } + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + { return marker } + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } + if (options.insertLeft) { marker.widgetNode.insertLeft = true; } + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + { throw new Error("Inserting collapsed marker partially overlapping an existing one") } + seeCollapsedSpans(); + } + + if (marker.addToHistory) + { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function (line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + { updateMaxLine = true; } + if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null), doc.cm && doc.cm.curOp); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { + if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } + }); } + + if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } + + if (marker.readOnly) { + seeReadOnlySpans(); + if (doc.history.done.length || doc.history.undone.length) + { doc.clearHistory(); } + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) { cm.curOp.updateMaxLine = true; } + if (marker.collapsed) + { regChange(cm, from.line, to.line + 1); } + else if (marker.className || marker.startStyle || marker.endStyle || marker.css || + marker.attributes || marker.title) + { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } + if (marker.atomic) { reCheckSelection(cm.doc); } + signalLater(cm, "markerAdded", cm, marker); + } + return marker + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + { markers[i].parent = this; } + }; + + SharedTextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + { this.markers[i].clear(); } + signalLater(this, "clear"); + }; + + SharedTextMarker.prototype.find = function (side, lineObj) { + return this.primary.find(side, lineObj) + }; + eventMixin(SharedTextMarker); + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function (doc) { + if (widget) { options.widgetNode = widget.cloneNode(true); } + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + { if (doc.linked[i].isParent) { return } } + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary) + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + var loop = function ( i ) { + var marker = markers[i], linked = [marker.primary.doc]; + linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + }; + + for (var i = 0; i < markers.length; i++) loop( i ); + } + + var nextDocId = 0; + var Doc = function(text, mode, firstLine, lineSep, direction) { + if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } + if (firstLine == null) { firstLine = 0; } + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.modeFrontier = this.highlightFrontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + this.lineSep = lineSep; + this.direction = (direction == "rtl") ? "rtl" : "ltr"; + this.extend = false; + + if (typeof text == "string") { text = this.splitLines(text); } + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) { this.iterN(from - this.first, to - from, op); } + else { this.iterN(this.first, this.first + this.size, from); } + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: this.splitLines(code), origin: "setValue", full: true}, true); + if (this.cm) { scrollToCoords(this.cm, 0, 0); } + setSelection(this, simpleSelection(top), sel_dontScroll); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) { return lines } + if (lineSep === '') { return lines.join('') } + return lines.join(lineSep || this.lineSeparator()) + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, + + getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, + getLineNumber: function(line) {return lineNo(line)}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") { line = getLine(this, line); } + return visualLine(line) + }, + + lineCount: function() {return this.size}, + firstLine: function() {return this.first}, + lastLine: function() {return this.first + this.size - 1}, + + clipPos: function(pos) {return clipPos(this, pos)}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") { pos = range.head; } + else if (start == "anchor") { pos = range.anchor; } + else if (start == "end" || start == "to" || start === false) { pos = range.to(); } + else { pos = range.from(); } + return pos + }, + listSelections: function() { return this.sel.ranges }, + somethingSelected: function() {return this.sel.somethingSelected()}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads), options); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) { return } + var out = []; + for (var i = 0; i < ranges.length; i++) + { out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head || ranges[i].anchor)); } + if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } + setSelection(this, normalizeSelection(this.cm, out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) { return lines } + else { return lines.join(lineSep || this.lineSeparator()) } + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) { sel = sel.join(lineSep || this.lineSeparator()); } + parts[i] = sel; + } + return parts + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + { dup[i] = code; } + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) + { makeChange(this, changes[i$1]); } + if (newSel) { setSelectionReplaceHistory(this, newSel); } + else if (this.cm) { ensureCursorVisible(this.cm); } + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } + for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } + return {undo: done, redo: undone} + }, + clearHistory: function() { + var this$1 = this; + + this.history = new History(this.history); + linkedDocs(this, function (doc) { return doc.history = this$1.history; }, true); + }, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } + return this.history.generation + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration) + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)} + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + setGutterMarker: docMethodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", function (line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) { line.gutterMarkers = null; } + return true + }) + }), + + clearGutter: docMethodOp(function(gutterID) { + var this$1 = this; + + this.iter(function (line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + changeLine(this$1, line, "gutter", function () { + line.gutterMarkers[gutterID] = null; + if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } + return true + }); + } + }); + }), + + lineInfo: function(line) { + var n; + if (typeof line == "number") { + if (!isLine(this, line)) { return null } + n = line; + line = getLine(this, line); + if (!line) { return null } + } else { + n = lineNo(line); + if (n == null) { return null } + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets} + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) { line[prop] = cls; } + else if (classTest(cls).test(line[prop])) { return false } + else { line[prop] += " " + cls; } + return true + }) + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) { return false } + else if (cls == null) { line[prop] = null; } + else { + var found = cur.match(classTest(cls)); + if (!found) { return false } + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true + }) + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options) + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark") + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + { markers.push(span.marker.parent || span.marker); } + } } + return markers + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function (line) { + var spans = line.markedSpans; + if (spans) { for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(span.to != null && lineNo == from.line && from.ch >= span.to || + span.from == null && lineNo != from.line || + span.from != null && lineNo == to.line && span.from >= to.ch) && + (!filter || filter(span.marker))) + { found.push(span.marker.parent || span.marker); } + } } + ++lineNo; + }); + return found + }, + getAllMarks: function() { + var markers = []; + this.iter(function (line) { + var sps = line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) + { if (sps[i].from != null) { markers.push(sps[i].marker); } } } + }); + return markers + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first, sepSize = this.lineSeparator().length; + this.iter(function (line) { + var sz = line.text.length + sepSize; + if (sz > off) { ch = off; return true } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)) + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) { return 0 } + var sepSize = this.lineSeparator().length; + this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value + index += line.text.length + sepSize; + }); + return index + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep, this.direction); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc + }, + + linkedDoc: function(options) { + if (!options) { options = {}; } + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) { from = options.from; } + if (options.to != null && options.to < to) { to = options.to; } + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); + if (options.sharedHist) { copy.history = this.history + ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) { other = other.doc; } + if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) { continue } + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break + } } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode}, + getEditor: function() {return this.cm}, + + splitLines: function(str) { + if (this.lineSep) { return str.split(this.lineSep) } + return splitLinesAuto(str) + }, + lineSeparator: function() { return this.lineSep || "\n" }, + + setDirection: docMethodOp(function (dir) { + if (dir != "rtl") { dir = "ltr"; } + if (dir == this.direction) { return } + this.direction = dir; + this.iter(function (line) { return line.order = null; }); + if (this.cm) { directionChanged(this.cm); } + }) + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + clearDragCursor(cm); + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + { return } + e_preventDefault(e); + if (ie) { lastDrop = +new Date; } + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || cm.isReadOnly()) { return } + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var markAsReadAndPasteIfAllFilesAreRead = function () { + if (++read == n) { + operation(cm, function () { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, + text: cm.doc.splitLines( + text.filter(function (t) { return t != null; }).join(cm.doc.lineSeparator())), + origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(clipPos(cm.doc, pos), clipPos(cm.doc, changeEnd(change)))); + })(); + } + }; + var readTextFromFile = function (file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + var reader = new FileReader; + reader.onerror = function () { return markAsReadAndPasteIfAllFilesAreRead(); }; + reader.onload = function () { + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + text[i] = content; + markAsReadAndPasteIfAllFilesAreRead(); + }; + reader.readAsText(file); + }; + for (var i = 0; i < files.length; i++) { readTextFromFile(files[i], i); } + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function () { return cm.display.input.focus(); }, 20); + return + } + try { + var text$1 = e.dataTransfer.getData("Text"); + if (text$1) { + var selected; + if (cm.state.draggingText && !cm.state.draggingText.copy) + { selected = cm.listSelections(); } + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) + { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } + cm.replaceSelection(text$1, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e$1){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } + + e.dataTransfer.setData("Text", cm.getSelection()); + e.dataTransfer.effectAllowed = "copyMove"; + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) { img.parentNode.removeChild(img); } + } + } + + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) { return } + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.getElementsByClassName) { return } + var byClass = document.getElementsByClassName("CodeMirror"), editors = []; + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) { editors.push(cm); } + } + if (editors.length) { editors[0].operation(function () { + for (var i = 0; i < editors.length; i++) { f(editors[i]); } + }); } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) { return } + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function () { + if (resizeTimer == null) { resizeTimer = setTimeout(function () { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); } + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function () { return forEachCodeMirror(onBlur); }); + } + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + var keyNames = { + 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 224: "Mod", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; + + // Number keys + for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } + // Alphabetic keys + for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } + // Function keys + for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } + + var keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + "fallthrough": "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", + "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", + "Ctrl-T": "transposeChars", "Ctrl-O": "openLine" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + "fallthrough": ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/); + name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } + else if (/^a(lt)?$/i.test(mod)) { alt = true; } + else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } + else if (/^s(hift)?$/i.test(mod)) { shift = true; } + else { throw new Error("Unrecognized modifier name: " + mod) } + } + if (alt) { name = "Alt-" + name; } + if (ctrl) { name = "Ctrl-" + name; } + if (cmd) { name = "Cmd-" + name; } + if (shift) { name = "Shift-" + name; } + return name + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + function normalizeKeyMap(keymap) { + var copy = {}; + for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } + if (value == "...") { delete keymap[keyname]; continue } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val = (void 0), name = (void 0); + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) { copy[name] = val; } + else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } + } + delete keymap[keyname]; + } } + for (var prop in copy) { keymap[prop] = copy[prop]; } + return keymap + } + + function lookupKey(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) { return "nothing" } + if (found === "...") { return "multi" } + if (found != null && handle(found)) { return "handled" } + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + { return lookupKey(key, map.fallthrough, handle, context) } + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) { return result } + } + } + } + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + function isModifierKey(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" + } + + function addModifierNames(name, event, noShift) { + var base = name; + if (event.altKey && base != "Alt") { name = "Alt-" + name; } + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Mod") { name = "Cmd-" + name; } + if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } + return name + } + + // Look up the name of a key as indicated by an event object. + function keyName(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) { return false } + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) { return false } + // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, + // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) + if (event.keyCode == 3 && event.code) { name = event.code; } + return addModifierNames(name, event, noShift) + } + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function () { + for (var i = kill.length - 1; i >= 0; i--) + { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } + ensureCursorVisible(cm); + }); + } + + function moveCharLogically(line, ch, dir) { + var target = skipExtendingChars(line.text, ch + dir, dir); + return target < 0 || target > line.text.length ? null : target + } + + function moveLogically(line, start, dir) { + var ch = moveCharLogically(line, start.ch, dir); + return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") + } + + function endOfLine(visually, cm, lineObj, lineNo, dir) { + if (visually) { + if (cm.doc.direction == "rtl") { dir = -dir; } + var order = getOrder(lineObj, cm.doc.direction); + if (order) { + var part = dir < 0 ? lst(order) : order[0]; + var moveInStorageOrder = (dir < 0) == (part.level == 1); + var sticky = moveInStorageOrder ? "after" : "before"; + var ch; + // With a wrapped rtl chunk (possibly spanning multiple bidi parts), + // it could be that the last bidi part is not on the last visual line, + // since visual lines contain content order-consecutive chunks. + // Thus, in rtl, we are looking for the first (content-order) character + // in the rtl chunk that is on the last line (that is, the same line + // as the last (content-order) character). + if (part.level > 0 || cm.doc.direction == "rtl") { + var prep = prepareMeasureForLine(cm, lineObj); + ch = dir < 0 ? lineObj.text.length - 1 : 0; + var targetTop = measureCharPrepared(cm, prep, ch).top; + ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); + if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } + } else { ch = dir < 0 ? part.to : part.from; } + return new Pos(lineNo, ch, sticky) + } + } + return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") + } + + function moveVisually(cm, line, start, dir) { + var bidi = getOrder(line, cm.doc.direction); + if (!bidi) { return moveLogically(line, start, dir) } + if (start.ch >= line.text.length) { + start.ch = line.text.length; + start.sticky = "before"; + } else if (start.ch <= 0) { + start.ch = 0; + start.sticky = "after"; + } + var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; + if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { + // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, + // nothing interesting happens. + return moveLogically(line, start, dir) + } + + var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; + var prep; + var getWrappedLineExtent = function (ch) { + if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } + prep = prep || prepareMeasureForLine(cm, line); + return wrappedLineExtentChar(cm, line, prep, ch) + }; + var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); + + if (cm.doc.direction == "rtl" || part.level == 1) { + var moveInStorageOrder = (part.level == 1) == (dir < 0); + var ch = mv(start, moveInStorageOrder ? 1 : -1); + if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { + // Case 2: We move within an rtl part or in an rtl editor on the same visual line + var sticky = moveInStorageOrder ? "before" : "after"; + return new Pos(start.line, ch, sticky) + } + } + + // Case 3: Could not move within this bidi part in this visual line, so leave + // the current bidi part + + var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { + var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder + ? new Pos(start.line, mv(ch, 1), "before") + : new Pos(start.line, ch, "after"); }; + + for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { + var part = bidi[partPos]; + var moveInStorageOrder = (dir > 0) == (part.level != 1); + var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); + if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } + ch = moveInStorageOrder ? part.from : mv(part.to, -1); + if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } + } + }; + + // Case 3a: Look for other bidi parts on the same visual line + var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); + if (res) { return res } + + // Case 3b: Look for other bidi parts on the next visual line + var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); + if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { + res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); + if (res) { return res } + } + + // Case 4: Nowhere to move + return null + } + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = { + selectAll: selectAll, + singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, + killLine: function (cm) { return deleteNearSelection(cm, function (range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + { return {from: range.head, to: Pos(range.head.line + 1, 0)} } + else + { return {from: range.head, to: Pos(range.head.line, len)} } + } else { + return {from: range.from(), to: range.to()} + } + }); }, + deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) + }); }); }, + delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), to: range.from() + }); }); }, + delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()} + }); }, + delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos } + }); }, + undo: function (cm) { return cm.undo(); }, + redo: function (cm) { return cm.redo(); }, + undoSelection: function (cm) { return cm.undoSelection(); }, + redoSelection: function (cm) { return cm.redoSelection(); }, + goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, + goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, + goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1} + ); }, + goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, + {origin: "+move", bias: 1} + ); }, + goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1} + ); }, + goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") + }, sel_move); }, + goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div") + }, sel_move); }, + goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } + return pos + }, sel_move); }, + goLineUp: function (cm) { return cm.moveV(-1, "line"); }, + goLineDown: function (cm) { return cm.moveV(1, "line"); }, + goPageUp: function (cm) { return cm.moveV(-1, "page"); }, + goPageDown: function (cm) { return cm.moveV(1, "page"); }, + goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, + goCharRight: function (cm) { return cm.moveH(1, "char"); }, + goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, + goColumnRight: function (cm) { return cm.moveH(1, "column"); }, + goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, + goGroupRight: function (cm) { return cm.moveH(1, "group"); }, + goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, + goWordRight: function (cm) { return cm.moveH(1, "word"); }, + delCharBefore: function (cm) { return cm.deleteH(-1, "codepoint"); }, + delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, + delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, + delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, + delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, + delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, + indentAuto: function (cm) { return cm.indentSelection("smart"); }, + indentMore: function (cm) { return cm.indentSelection("add"); }, + indentLess: function (cm) { return cm.indentSelection("subtract"); }, + insertTab: function (cm) { return cm.replaceSelection("\t"); }, + insertSoftTab: function (cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(spaceStr(tabSize - col % tabSize)); + } + cm.replaceSelections(spaces); + }, + defaultTab: function (cm) { + if (cm.somethingSelected()) { cm.indentSelection("add"); } + else { cm.execCommand("insertTab"); } + }, + // Swap the two chars left and right of each selection's head. + // Move cursor behind the two swapped characters afterwards. + // + // Doesn't consider line feeds a character. + // Doesn't scan more than one line above to find a character. + // Doesn't do anything on an empty line. + // Doesn't do anything with non-empty selections. + transposeChars: function (cm) { return runInOp(cm, function () { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) { continue } + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) { + cur = new Pos(cur.line, 1); + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); + } + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); }, + newlineAndIndent: function (cm) { return runInOp(cm, function () { + var sels = cm.listSelections(); + for (var i = sels.length - 1; i >= 0; i--) + { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } + sels = cm.listSelections(); + for (var i$1 = 0; i$1 < sels.length; i$1++) + { cm.indentLine(sels[i$1].from().line, null, true); } + ensureCursorVisible(cm); + }); }, + openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, + toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } + }; + + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, visual, lineN, 1) + } + function lineEnd(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLineEnd(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, line, lineN, -1) + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line, cm.doc.direction); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(start.ch, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) + } + return start + } + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) { return false } + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + if (dropShift) { cm.display.shift = false; } + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) { return result } + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm) + } + + // Note that, despite the name, this function is also used to check + // for bound mouse clicks. + + var stopSeq = new Delayed; + + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) { return "handled" } + if (/\'$/.test(name)) + { cm.state.keySeq = null; } + else + { stopSeq.set(50, function () { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); } + if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } + } + return dispatchKeyInner(cm, name, e, handle) + } + + function dispatchKeyInner(cm, name, e, handle) { + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + { cm.state.keySeq = name; } + if (result == "handled") + { signalLater(cm, "keyHandled", cm, name, e); } + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + return !!result + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) { return false } + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) + || dispatchKey(cm, name, e, function (b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + { return doHandleBinding(cm, b) } + }) + } else { + return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) { return } + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + { cm.replaceSelection("", null, "cut"); } + } + if (gecko && !mac && !handled && code == 46 && e.shiftKey && !e.ctrlKey && document.execCommand) + { document.execCommand("cut"); } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + { showCrossHair(cm); } + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) { this.doc.sel.shift = false; } + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + // Some browsers fire keypress events for backspace + if (ch == "\x08") { return } + if (handleCharBinding(cm, e, ch)) { return } + cm.display.input.onKeyPress(e); + } + + var DOUBLECLICK_DELAY = 400; + + var PastClick = function(time, pos, button) { + this.time = time; + this.pos = pos; + this.button = button; + }; + + PastClick.prototype.compare = function (time, pos, button) { + return this.time + DOUBLECLICK_DELAY > time && + cmp(pos, this.pos) == 0 && button == this.button + }; + + var lastClick, lastDoubleClick; + function clickRepeat(pos, button) { + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { + lastClick = lastDoubleClick = null; + return "triple" + } else if (lastClick && lastClick.compare(now, pos, button)) { + lastDoubleClick = new PastClick(now, pos, button); + lastClick = null; + return "double" + } else { + lastClick = new PastClick(now, pos, button); + lastDoubleClick = null; + return "single" + } + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } + display.input.ensurePolled(); + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function () { return display.scroller.draggable = true; }, 100); + } + return + } + if (clickInGutter(cm, e)) { return } + var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; + window.focus(); + + // #3261: make sure, that we're not starting a second selection + if (button == 1 && cm.state.selectingText) + { cm.state.selectingText(e); } + + if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } + + if (button == 1) { + if (pos) { leftButtonDown(cm, pos, repeat, e); } + else if (e_target(e) == display.scroller) { e_preventDefault(e); } + } else if (button == 2) { + if (pos) { extendSelection(cm.doc, pos); } + setTimeout(function () { return display.input.focus(); }, 20); + } else if (button == 3) { + if (captureRightClick) { cm.display.input.onContextMenu(e); } + else { delayBlurEvent(cm); } + } + } + + function handleMappedButton(cm, button, pos, repeat, event) { + var name = "Click"; + if (repeat == "double") { name = "Double" + name; } + else if (repeat == "triple") { name = "Triple" + name; } + name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; + + return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { + if (typeof bound == "string") { bound = commands[bound]; } + if (!bound) { return false } + var done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + done = bound(cm, pos) != Pass; + } finally { + cm.state.suppressEdits = false; + } + return done + }) + } + + function configureMouse(cm, repeat, event) { + var option = cm.getOption("configureMouse"); + var value = option ? option(cm, repeat, event) : {}; + if (value.unit == null) { + var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; + value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; + } + if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } + if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } + if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } + return value + } + + function leftButtonDown(cm, pos, repeat, event) { + if (ie) { setTimeout(bind(ensureFocus, cm), 0); } + else { cm.curOp.focus = activeElt(); } + + var behavior = configureMouse(cm, repeat, event); + + var sel = cm.doc.sel, contained; + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && + repeat == "single" && (contained = sel.contains(pos)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && + (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) + { leftButtonStartDrag(cm, event, pos, behavior); } + else + { leftButtonSelect(cm, event, pos, behavior); } + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, event, pos, behavior) { + var display = cm.display, moved = false; + var dragEnd = operation(cm, function (e) { + if (webkit) { display.scroller.draggable = false; } + cm.state.draggingText = false; + if (cm.state.delayingBlurEvent) { + if (cm.hasFocus()) { cm.state.delayingBlurEvent = false; } + else { delayBlurEvent(cm); } + } + off(display.wrapper.ownerDocument, "mouseup", dragEnd); + off(display.wrapper.ownerDocument, "mousemove", mouseMove); + off(display.scroller, "dragstart", dragStart); + off(display.scroller, "drop", dragEnd); + if (!moved) { + e_preventDefault(e); + if (!behavior.addNew) + { extendSelection(cm.doc, pos, null, null, behavior.extend); } + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if ((webkit && !safari) || ie && ie_version == 9) + { setTimeout(function () {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus();}, 20); } + else + { display.input.focus(); } + } + }); + var mouseMove = function(e2) { + moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; + }; + var dragStart = function () { return moved = true; }; + // Let the drag handler handle this. + if (webkit) { display.scroller.draggable = true; } + cm.state.draggingText = dragEnd; + dragEnd.copy = !behavior.moveOnDrag; + on(display.wrapper.ownerDocument, "mouseup", dragEnd); + on(display.wrapper.ownerDocument, "mousemove", mouseMove); + on(display.scroller, "dragstart", dragStart); + on(display.scroller, "drop", dragEnd); + + cm.state.delayingBlurEvent = true; + setTimeout(function () { return display.input.focus(); }, 20); + // IE's approach to draggable + if (display.scroller.dragDrop) { display.scroller.dragDrop(); } + } + + function rangeForUnit(cm, pos, unit) { + if (unit == "char") { return new Range(pos, pos) } + if (unit == "word") { return cm.findWordAt(pos) } + if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } + var result = unit(cm, pos); + return new Range(result.from, result.to) + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, event, start, behavior) { + if (ie) { delayBlurEvent(cm); } + var display = cm.display, doc = cm.doc; + e_preventDefault(event); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (behavior.addNew && !behavior.extend) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + { ourRange = ranges[ourIndex]; } + else + { ourRange = new Range(start, start); } + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (behavior.unit == "rectangle") { + if (!behavior.addNew) { ourRange = new Range(start, start); } + start = posFromMouse(cm, event, true, true); + ourIndex = -1; + } else { + var range = rangeForUnit(cm, start, behavior.unit); + if (behavior.extend) + { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend); } + else + { ourRange = range; } + } + + if (!behavior.addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { + setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) { return } + lastPos = pos; + + if (behavior.unit == "rectangle") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } + else if (text.length > leftPos) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } + } + if (!ranges.length) { ranges.push(new Range(start, start)); } + setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var range = rangeForUnit(cm, pos, behavior.unit); + var anchor = oldRange.anchor, head; + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + var ranges$1 = startSel.ranges.slice(0); + ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); + setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); + if (!cur) { return } + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) { setTimeout(operation(cm, function () { + if (counter != curCount) { return } + display.scroller.scrollTop += outside; + extend(e); + }), 50); } + } + } + + function done(e) { + cm.state.selectingText = false; + counter = Infinity; + // If e is null or undefined we interpret this as someone trying + // to explicitly cancel the selection rather than the user + // letting go of the mouse button. + if (e) { + e_preventDefault(e); + display.input.focus(); + } + off(display.wrapper.ownerDocument, "mousemove", move); + off(display.wrapper.ownerDocument, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function (e) { + if (e.buttons === 0 || !e_button(e)) { done(e); } + else { extend(e); } + }); + var up = operation(cm, done); + cm.state.selectingText = up; + on(display.wrapper.ownerDocument, "mousemove", move); + on(display.wrapper.ownerDocument, "mouseup", up); + } + + // Used when mouse-selecting to adjust the anchor to the proper side + // of a bidi jump depending on the visual position of the head. + function bidiSimplify(cm, range) { + var anchor = range.anchor; + var head = range.head; + var anchorLine = getLine(cm.doc, anchor.line); + if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } + var order = getOrder(anchorLine); + if (!order) { return range } + var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; + if (part.from != anchor.ch && part.to != anchor.ch) { return range } + var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); + if (boundary == 0 || boundary == order.length) { return range } + + // Compute the relative visual position of the head compared to the + // anchor (<0 is to the left, >0 to the right) + var leftSide; + if (head.line != anchor.line) { + leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; + } else { + var headIndex = getBidiPartAt(order, head.ch, head.sticky); + var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); + if (headIndex == boundary - 1 || headIndex == boundary) + { leftSide = dir < 0; } + else + { leftSide = dir > 0; } + } + + var usePart = order[boundary + (leftSide ? -1 : 0)]; + var from = leftSide == (usePart.level == 1); + var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; + return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) + } + + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent) { + var mX, mY; + if (e.touches) { + mX = e.touches[0].clientX; + mY = e.touches[0].clientY; + } else { + try { mX = e.clientX; mY = e.clientY; } + catch(e$1) { return false } + } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } + if (prevent) { e_preventDefault(e); } + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.display.gutterSpecs.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.display.gutterSpecs[i]; + signal(cm, type, cm, line, gutter.className, e); + return e_defaultPrevented(e) + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true) + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } + if (signalDOMEvent(cm, e, "contextmenu")) { return } + if (!captureRightClick) { cm.display.input.onContextMenu(e); } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) { return false } + return gutterEvent(cm, e, "gutterContextMenu", false) + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + var Init = {toString: function(){return "CodeMirror.Init"}}; + + var defaults = {}; + var optionHandlers = {}; + + function defineOptions(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) { optionHandlers[name] = + notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } + } + + CodeMirror.defineOption = option; + + // Passed to option handlers when there is no old value. + CodeMirror.Init = Init; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function (cm, val) { return cm.setValue(val); }, true); + option("mode", null, function (cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function (cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + + option("lineSeparator", null, function (cm, val) { + cm.doc.lineSep = val; + if (!val) { return } + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function (line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) { break } + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } + }); + option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != Init) { cm.refresh(); } + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function () { + throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME + }, true); + option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); + option("autocorrect", false, function (cm, val) { return cm.getInputField().autocorrect = val; }, true); + option("autocapitalize", false, function (cm, val) { return cm.getInputField().autocapitalize = val; }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function (cm) { + themeChanged(cm); + updateGutters(cm); + }, true); + option("keyMap", "default", function (cm, val, old) { + var next = getKeyMap(val); + var prev = old != Init && getKeyMap(old); + if (prev && prev.detach) { prev.detach(cm, next); } + if (next.attach) { next.attach(cm, prev || null); } + }); + option("extraKeys", null); + option("configureMouse", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function (cm, val) { + cm.display.gutterSpecs = getGutters(val, cm.options.lineNumbers); + updateGutters(cm); + }, true); + option("fixedGutter", true, function (cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); + option("scrollbarStyle", "native", function (cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function (cm, val) { + cm.display.gutterSpecs = getGutters(cm.options.gutters, val); + updateGutters(cm); + }, true); + option("firstLineNumber", 1, updateGutters, true); + option("lineNumberFormatter", function (integer) { return integer; }, updateGutters, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + option("pasteLinesPerSelection", true); + option("selectionsMayTouch", false); + + option("readOnly", false, function (cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + } + cm.display.input.readOnlyChanged(val); + }); + + option("screenReaderLabel", null, function (cm, val) { + val = (val === '') ? null : val; + cm.display.input.screenReaderLabelChanged(val); + }); + + option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); + option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function (cm, val) { + if (!val) { cm.display.input.resetPosition(); } + }); + + option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); + option("autofocus", null); + option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); + option("phrases", null); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function () { return updateScrollbars(cm); }, 100); + } + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + var this$1 = this; + + if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + + var doc = options.value; + if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } + else if (options.mode) { doc.modeOption = options.mode; } + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input, options); + display.wrapper.CodeMirror = this; + themeChanged(this); + if (options.lineWrapping) + { this.display.wrapper.className += " CodeMirror-wrap"; } + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll + selectingText: false, + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + if (options.autofocus && !mobile) { display.input.focus(); } + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || this.hasFocus()) + { setTimeout(function () { + if (this$1.hasFocus() && !this$1.state.focused) { onFocus(this$1); } + }, 20); } + else + { onBlur(this); } + + for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) + { optionHandlers[opt](this, options[opt], Init); } } + maybeUpdateLineNumberWidth(this); + if (options.finishInit) { options.finishInit(this); } + for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this); } + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + { display.lineDiv.style.textRendering = "auto"; } + } + + // The default configuration options. + CodeMirror.defaults = defaults; + // Functions to run when options are changed. + CodeMirror.optionHandlers = optionHandlers; + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + { on(d.scroller, "dblclick", operation(cm, function (e) { + if (signalDOMEvent(cm, e)) { return } + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); } + else + { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); + on(d.input.getField(), "contextmenu", function (e) { + if (!d.scroller.contains(e.target)) { onContextMenu(cm, e); } + }); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + } + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) { return false } + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1 + } + function farAway(touch, other) { + if (other.left == null) { return true } + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20 + } + on(d.scroller, "touchstart", function (e) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { + d.input.ensurePolled(); + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function () { + if (d.activeTouch) { d.activeTouch.moved = true; } + }); + on(d.scroller, "touchend", function (e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + { range = new Range(pos, pos); } + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + { range = cm.findWordAt(pos); } + else // Triple tap + { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function () { + if (d.scroller.clientHeight) { + updateScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); + on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, + over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, + start: function (e) { return onDragStart(cm, e); }, + drop: operation(cm, onDrop), + leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} + }; + + var inp = d.input.getField(); + on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", function (e) { return onFocus(cm, e); }); + on(inp, "blur", function (e) { return onBlur(cm, e); }); + } + + var initHooks = []; + CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) { how = "add"; } + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) { how = "prev"; } + else { state = getContextBefore(cm, n).state; } + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) { line.stateAfter = null; } + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) { return } + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } + else { indentation = 0; } + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } + if (pos < indentation) { indentString += spaceStr(indentation - pos); } + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { + var range = doc.sel.ranges[i$1]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos$1 = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); + break + } + } + } + } + + // This will be set to a {lineWise: bool, text: [string]} object, so + // that, when pasting, we know what kind of selections the copied + // text was made out of. + var lastCopied = null; + + function setLastCopied(newLastCopied) { + lastCopied = newLastCopied; + } + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) { sel = doc.sel; } + + var recent = +new Date - 200; + var paste = origin == "paste" || cm.state.pasteIncoming > recent; + var textLines = splitLinesAuto(inserted), multiPaste = null; + // When pasting N lines into N selections, insert one line per selection + if (paste && sel.ranges.length > 1) { + if (lastCopied && lastCopied.text.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.text.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.text.length; i++) + { multiPaste.push(doc.splitLines(lastCopied.text[i])); } + } + } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { + multiPaste = map(textLines, function (l) { return [l]; }); + } + } + + var updateInput = cm.curOp.updateInput; + // Normal behavior is to insert the new text into every selection + for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { + var range = sel.ranges[i$1]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + { from = Pos(from.line, from.ch - deleted); } + else if (cm.state.overwrite && !paste) // Handle overwrite + { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } + else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n")) + { from = to = Pos(from.line, 0); } + } + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, + origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !paste) + { triggerElectric(cm, inserted); } + + ensureCursorVisible(cm); + if (cm.curOp.updateInput < 2) { cm.curOp.updateInput = updateInput; } + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = -1; + } + + function handlePaste(e, cm) { + var pasted = e.clipboardData && e.clipboardData.getData("Text"); + if (pasted) { + e.preventDefault(); + if (!cm.isReadOnly() && !cm.options.disableInput) + { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } + return true + } + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) { return } + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } + var mode = cm.getModeAt(range.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range.head.line, "smart"); + break + } } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + { indented = indentLine(cm, range.head.line, "smart"); } + } + if (indented) { signalLater(cm, "electricInput", cm, range.head.line); } + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges} + } + + function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { + field.setAttribute("autocorrect", autocorrect ? "" : "off"); + field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); + field.setAttribute("spellcheck", !!spellcheck); + } + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; min-height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) { te.style.width = "1000px"; } + else { te.setAttribute("wrap", "off"); } + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) { te.style.border = "1px solid black"; } + disableBrowserMagic(te); + return div + } + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + function addEditorMethods(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + var helpers = CodeMirror.helpers = {}; + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") { return } + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + { operation(this, optionHandlers[option])(this, value, old); } + signal(this, "optionChange", this, option); + }, + + getOption: function(option) {return this.options[option]}, + getDoc: function() {return this.doc}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + { if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true + } } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) { throw new Error("Overlays may not be stateful.") } + insertSorted(this.state.overlays, + {mode: mode, modeSpec: spec, opaque: options && options.opaque, + priority: (options && options.priority) || 0}, + function (overlay) { return overlay.priority; }); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } + else { dir = dir ? "add" : "subtract"; } + } + if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + { indentLine(this, j, how); } + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + { replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) { ensureCursorVisible(this); } + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise) + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true) + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) { type = styles[2]; } + else { for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } + else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } + else { type = styles[mid * 2 + 2]; break } + } } + var cut = type ? type.indexOf("overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) { return mode } + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0] + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) { return found } + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) { found.push(help[mode[type]]); } + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) { found.push(val); } + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i$1 = 0; i$1 < help._global.length; i$1++) { + var cur = help._global[i$1]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + { found.push(cur.val); } + } + return found + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getContextBefore(this, line + 1, precise).state + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) { pos = range.head; } + else if (typeof start == "object") { pos = clipPos(this.doc, start); } + else { pos = start ? range.from() : range.to(); } + return cursorCoords(this, pos, mode || "page") + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page") + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top) + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset) + }, + heightAtLine: function(line, mode, includeWidgets) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) { line = this.doc.first; } + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + + (end ? this.doc.height - heightAtLine(lineObj) : 0) + }, + + defaultTextHeight: function() { return textHeight(this.display) }, + defaultCharWidth: function() { return charWidth(this.display) }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + { top = pos.top - node.offsetHeight; } + else if (pos.bottom + node.offsetHeight <= vspace) + { top = pos.bottom; } + if (left + node.offsetWidth > hspace) + { left = hspace - node.offsetWidth; } + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") { left = 0; } + else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } + node.style.left = left + "px"; + } + if (scroll) + { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + triggerOnMouseDown: methodOp(onMouseDown), + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + { return commands[cmd].call(null, this) } + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) { break } + } + return cur + }, + + moveH: methodOp(function(dir, unit) { + var this$1 = this; + + this.extendSelectionsBy(function (range) { + if (this$1.display.shift || this$1.doc.extend || range.empty()) + { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } + else + { return dir < 0 ? range.from() : range.to() } + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + { doc.replaceSelection("", null, "+delete"); } + else + { deleteNearSelection(this, function (range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} + }); } + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) { x = coords.left; } + else { coords.left = x; } + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) { break } + } + return cur + }, + + moveV: methodOp(function(dir, unit) { + var this$1 = this; + + var doc = this.doc, goals = []; + var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function (range) { + if (collapse) + { return dir < 0 ? range.from() : range.to() } + var headPos = cursorCoords(this$1, range.head, "div"); + if (range.goalColumn != null) { headPos.left = range.goalColumn; } + goals.push(headPos.left); + var pos = findPosV(this$1, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } + return pos + }, sel_move); + if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) + { doc.sel.ranges[i].goalColumn = goals[i]; } } + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function (ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } + : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; + while (start > 0 && check(line.charAt(start - 1))) { --start; } + while (end < line.length && check(line.charAt(end))) { ++end; } + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)) + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) { return } + if (this.state.overwrite = !this.state.overwrite) + { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + else + { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt() }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, + + scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)} + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) { margin = this.options.cursorScrollMargin; } + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) { range.to = range.from; } + range.margin = margin || 0; + + if (range.from.line != null) { + scrollToRange(this, range); + } else { + scrollToCoordsRange(this, range.from, range.to, range.margin); + } + }), + + setSize: methodOp(function(width, height) { + var this$1 = this; + + var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; + if (width != null) { this.display.wrapper.style.width = interpret(width); } + if (height != null) { this.display.wrapper.style.height = interpret(height); } + if (this.options.lineWrapping) { clearLineMeasurementCache(this); } + var lineNo = this.display.viewFrom; + this.doc.iter(lineNo, this.display.viewTo, function (line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) + { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } + ++lineNo; + }); + this.curOp.forceUpdate = true; + signal(this, "refresh", this); + }), + + operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this.display); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) + { estimateLineHeights(this); } + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + // Cancel the current text selection if any (#5821) + if (this.state.selectingText) { this.state.selectingText(); } + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + scrollToCoords(this, doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old + }), + + phrase: function(phraseText) { + var phrases = this.options.phrases; + return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText + }, + + getInputField: function(){return this.display.input.getField()}, + getWrapperElement: function(){return this.display.wrapper}, + getScrollerElement: function(){return this.display.scroller}, + getGutterElement: function(){return this.display.gutters} + }; + eventMixin(CodeMirror); + + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "codepoint", "char", "column" (like char, but + // doesn't cross line boundaries), "word" (across next word), or + // "group" (to the start of next group of word or + // non-word-non-whitespace chars). The visually param controls + // whether, in right-to-left text, direction 1 means to move towards + // the next index in the string, or towards the character to the right + // of the current position. The resulting position will have a + // hitSide=true property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var oldPos = pos; + var origDir = dir; + var lineObj = getLine(doc, pos.line); + var lineDir = visually && doc.direction == "rtl" ? -dir : dir; + function findNextLine() { + var l = pos.line + lineDir; + if (l < doc.first || l >= doc.first + doc.size) { return false } + pos = new Pos(l, pos.ch, pos.sticky); + return lineObj = getLine(doc, l) + } + function moveOnce(boundToLine) { + var next; + if (unit == "codepoint") { + var ch = lineObj.text.charCodeAt(pos.ch + (dir > 0 ? 0 : -1)); + if (isNaN(ch)) { + next = null; + } else { + var astral = dir > 0 ? ch >= 0xD800 && ch < 0xDC00 : ch >= 0xDC00 && ch < 0xDFFF; + next = new Pos(pos.line, Math.max(0, Math.min(lineObj.text.length, pos.ch + dir * (astral ? 2 : 1))), -dir); + } + } else if (visually) { + next = moveVisually(doc.cm, lineObj, pos, dir); + } else { + next = moveLogically(lineObj, pos, dir); + } + if (next == null) { + if (!boundToLine && findNextLine()) + { pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir); } + else + { return false } + } else { + pos = next; + } + return true + } + + if (unit == "char" || unit == "codepoint") { + moveOnce(); + } else if (unit == "column") { + moveOnce(true); + } else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) { break } + var cur = lineObj.text.charAt(pos.ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) { type = "s"; } + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} + break + } + + if (type) { sawType = type; } + if (dir > 0 && !moveOnce(!first)) { break } + } + } + var result = skipAtomic(doc, pos, oldPos, origDir, true); + if (equalCursorPos(oldPos, result)) { result.hitSide = true; } + return result + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); + y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; + + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + var target; + for (;;) { + target = coordsChar(cm, x, y); + if (!target.outside) { break } + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } + y += dir * 5; + } + return target + } + + // CONTENTEDITABLE INPUT STYLE + + var ContentEditableInput = function(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.composing = null; + this.gracePeriod = false; + this.readDOMTimeout = null; + }; + + ContentEditableInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + div.contentEditable = true; + disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize); + + function belongsToInput(e) { + for (var t = e.target; t; t = t.parentNode) { + if (t == div) { return true } + if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) { break } + } + return false + } + + on(div, "paste", function (e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + // IE doesn't fire input events, so we schedule a read for the pasted content in this way + if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } + }); + + on(div, "compositionstart", function (e) { + this$1.composing = {data: e.data, done: false}; + }); + on(div, "compositionupdate", function (e) { + if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } + }); + on(div, "compositionend", function (e) { + if (this$1.composing) { + if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } + this$1.composing.done = true; + } + }); + + on(div, "touchstart", function () { return input.forceCompositionEnd(); }); + + on(div, "input", function () { + if (!this$1.composing) { this$1.readFromDOMSoon(); } + }); + + function onCopyCut(e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.operation(function () { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + if (e.clipboardData) { + e.clipboardData.clearData(); + var content = lastCopied.text.join("\n"); + // iOS exposes the clipboard API, but seems to discard content inserted into it + e.clipboardData.setData("Text", content); + if (e.clipboardData.getData("Text") == content) { + e.preventDefault(); + return + } + } + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.text.join("\n"); + var hadFocus = activeElt(); + selectInput(te); + setTimeout(function () { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + if (hadFocus == div) { input.showPrimarySelection(); } + }, 50); + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }; + + ContentEditableInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.div.setAttribute('aria-label', label); + } else { + this.div.removeAttribute('aria-label'); + } + }; + + ContentEditableInput.prototype.prepareSelection = function () { + var result = prepareSelection(this.cm, false); + result.focus = activeElt() == this.div; + return result + }; + + ContentEditableInput.prototype.showSelection = function (info, takeFocus) { + if (!info || !this.cm.display.view.length) { return } + if (info.focus || takeFocus) { this.showPrimarySelection(); } + this.showMultipleSelections(info); + }; + + ContentEditableInput.prototype.getSelection = function () { + return this.cm.display.wrapper.ownerDocument.getSelection() + }; + + ContentEditableInput.prototype.showPrimarySelection = function () { + var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); + var from = prim.from(), to = prim.to(); + + if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { + sel.removeAllRanges(); + return + } + + var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), from) == 0 && + cmp(maxPos(curAnchor, curFocus), to) == 0) + { return } + + var view = cm.display.view; + var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || + {node: view[0].measure.map[2], offset: 0}; + var end = to.line < cm.display.viewTo && posToDOM(cm, to); + if (!end) { + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + if (!start || !end) { + sel.removeAllRanges(); + return + } + + var old = sel.rangeCount && sel.getRangeAt(0), rng; + try { rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + if (!gecko && cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) { + sel.removeAllRanges(); + sel.addRange(rng); + } + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } + if (old && sel.anchorNode == null) { sel.addRange(old); } + else if (gecko) { this.startGracePeriod(); } + } + this.rememberSelection(); + }; + + ContentEditableInput.prototype.startGracePeriod = function () { + var this$1 = this; + + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function () { + this$1.gracePeriod = false; + if (this$1.selectionChanged()) + { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } + }, 20); + }; + + ContentEditableInput.prototype.showMultipleSelections = function (info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }; + + ContentEditableInput.prototype.rememberSelection = function () { + var sel = this.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }; + + ContentEditableInput.prototype.selectionInEditor = function () { + var sel = this.getSelection(); + if (!sel.rangeCount) { return false } + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node) + }; + + ContentEditableInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor") { + if (!this.selectionInEditor() || activeElt() != this.div) + { this.showSelection(this.prepareSelection(), true); } + this.div.focus(); + } + }; + ContentEditableInput.prototype.blur = function () { this.div.blur(); }; + ContentEditableInput.prototype.getField = function () { return this.div }; + + ContentEditableInput.prototype.supportsTouch = function () { return true }; + + ContentEditableInput.prototype.receivedFocus = function () { + var this$1 = this; + + var input = this; + if (this.selectionInEditor()) + { setTimeout(function () { return this$1.pollSelection(); }, 20); } + else + { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }; + + ContentEditableInput.prototype.selectionChanged = function () { + var sel = this.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset + }; + + ContentEditableInput.prototype.pollSelection = function () { + if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } + var sel = this.getSelection(), cm = this.cm; + // On Android Chrome (version 56, at least), backspacing into an + // uneditable block element will put the cursor in that element, + // and then, because it's not editable, hide the virtual keyboard. + // Because Android doesn't allow us to actually detect backspace + // presses in a sane way, this code checks for when that happens + // and simulates a backspace press in this case. + if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { + this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); + this.blur(); + this.focus(); + return + } + if (this.composing) { return } + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) { runInOp(cm, function () { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } + }); } + }; + + ContentEditableInput.prototype.pollContent = function () { + if (this.readDOMTimeout != null) { + clearTimeout(this.readDOMTimeout); + this.readDOMTimeout = null; + } + + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.ch == 0 && from.line > cm.firstLine()) + { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } + if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) + { to = Pos(to.line + 1, 0); } + if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } + + var fromIndex, fromLine, fromNode; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + fromLine = lineNo(display.view[0].line); + fromNode = display.view[0].node; + } else { + fromLine = lineNo(display.view[fromIndex].line); + fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + var toLine, toNode; + if (toIndex == display.view.length - 1) { + toLine = display.viewTo - 1; + toNode = display.lineDiv.lastChild; + } else { + toLine = lineNo(display.view[toIndex + 1].line) - 1; + toNode = display.view[toIndex + 1].node.previousSibling; + } + + if (!fromNode) { return false } + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else { break } + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + { ++cutFront; } + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + { ++cutEnd; } + // Try to move start of change to start of selection if ambiguous + if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { + while (cutFront && cutFront > from.ch && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { + cutFront--; + cutEnd++; + } + } + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); + newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true + } + }; + + ContentEditableInput.prototype.ensurePolled = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.reset = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.forceCompositionEnd = function () { + if (!this.composing) { return } + clearTimeout(this.readDOMTimeout); + this.composing = null; + this.updateFromDOM(); + this.div.blur(); + this.div.focus(); + }; + ContentEditableInput.prototype.readFromDOMSoon = function () { + var this$1 = this; + + if (this.readDOMTimeout != null) { return } + this.readDOMTimeout = setTimeout(function () { + this$1.readDOMTimeout = null; + if (this$1.composing) { + if (this$1.composing.done) { this$1.composing = null; } + else { return } + } + this$1.updateFromDOM(); + }, 80); + }; + + ContentEditableInput.prototype.updateFromDOM = function () { + var this$1 = this; + + if (this.cm.isReadOnly() || !this.pollContent()) + { runInOp(this.cm, function () { return regChange(this$1.cm); }); } + }; + + ContentEditableInput.prototype.setUneditable = function (node) { + node.contentEditable = "false"; + }; + + ContentEditableInput.prototype.onKeyPress = function (e) { + if (e.charCode == 0 || this.composing) { return } + e.preventDefault(); + if (!this.cm.isReadOnly()) + { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } + }; + + ContentEditableInput.prototype.readOnlyChanged = function (val) { + this.div.contentEditable = String(val != "nocursor"); + }; + + ContentEditableInput.prototype.onContextMenu = function () {}; + ContentEditableInput.prototype.resetPosition = function () {}; + + ContentEditableInput.prototype.needsContentAttribute = true; + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) { return null } + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line, cm.doc.direction), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result + } + + function isInGutter(node) { + for (var scan = node; scan; scan = scan.parentNode) + { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } + return false + } + + function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; + function recognizeMarker(id) { return function (marker) { return marker.id == id; } } + function close() { + if (closing) { + text += lineSep; + if (extraLinebreak) { text += lineSep; } + closing = extraLinebreak = false; + } + } + function addText(str) { + if (str) { + close(); + text += str; + } + } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText) { + addText(cmText); + return + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find(0))) + { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)); } + return + } + if (node.getAttribute("contenteditable") == "false") { return } + var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); + if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } + + if (isBlock) { close(); } + for (var i = 0; i < node.childNodes.length; i++) + { walk(node.childNodes[i]); } + + if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } + if (isBlock) { closing = true; } + } else if (node.nodeType == 3) { + addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); + } + } + for (;;) { + walk(from); + if (from == to) { break } + from = from.nextSibling; + extraLinebreak = false; + } + return text + } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) { return null } + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + { return locateNodeInLineView(lineView, node, offset) } + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad) + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) { offset = textNode.nodeValue.length; } + } + while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)]; } + return Pos(line, ch) + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) { return badPos(found, bad) } + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + { return badPos(Pos(found.line, found.ch - dist), bad) } + else + { dist += after.textContent.length; } + } + for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + { return badPos(Pos(found.line, found.ch + dist$1), bad) } + else + { dist$1 += before.textContent.length; } + } + } + + // TEXTAREA INPUT STYLE + + var TextareaInput = function(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + TextareaInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = this.cm; + this.createField(display); + var te = this.textarea; + + display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) { te.style.width = "0px"; } + + on(te, "input", function () { + if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } + input.poll(); + }); + + on(te, "paste", function (e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + + cm.state.pasteIncoming = +new Date; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") { cm.state.cutIncoming = +new Date; } + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function (e) { + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } + if (!te.dispatchEvent) { + cm.state.pasteIncoming = +new Date; + input.focus(); + return + } + + // Pass the `paste` event to the textarea so it's handled by its event listener. + var event = new Event("paste"); + event.clipboardData = e.clipboardData; + te.dispatchEvent(event); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function (e) { + if (!eventInWidget(display, e)) { e_preventDefault(e); } + }); + + on(te, "compositionstart", function () { + var start = cm.getCursor("from"); + if (input.composing) { input.composing.range.clear(); } + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function () { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }; + + TextareaInput.prototype.createField = function (_display) { + // Wraps and hides input textarea + this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + this.textarea = this.wrapper.firstChild; + }; + + TextareaInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.textarea.setAttribute('aria-label', label); + } else { + this.textarea.removeAttribute('aria-label'); + } + }; + + TextareaInput.prototype.prepareSelection = function () { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result + }; + + TextareaInput.prototype.showSelection = function (drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }; + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + TextareaInput.prototype.reset = function (typing) { + if (this.contextMenuPending || this.composing) { return } + var cm = this.cm; + if (cm.somethingSelected()) { + this.prevInput = ""; + var content = cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) { selectInput(this.textarea); } + if (ie && ie_version >= 9) { this.hasSelection = content; } + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) { this.hasSelection = null; } + } + }; + + TextareaInput.prototype.getField = function () { return this.textarea }; + + TextareaInput.prototype.supportsTouch = function () { return false }; + + TextareaInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }; + + TextareaInput.prototype.blur = function () { this.textarea.blur(); }; + + TextareaInput.prototype.resetPosition = function () { + this.wrapper.style.top = this.wrapper.style.left = 0; + }; + + TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + TextareaInput.prototype.slowPoll = function () { + var this$1 = this; + + if (this.pollingFast) { return } + this.polling.set(this.cm.options.pollInterval, function () { + this$1.poll(); + if (this$1.cm.state.focused) { this$1.slowPoll(); } + }); + }; + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + TextareaInput.prototype.fastPoll = function () { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + TextareaInput.prototype.poll = function () { + var this$1 = this; + + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (this.contextMenuPending || !cm.state.focused || + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) + { return false } + + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) { return false } + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } + + runInOp(cm, function () { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, this$1.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } + else { this$1.prevInput = text; } + + if (this$1.composing) { + this$1.composing.range.clear(); + this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true + }; + + TextareaInput.prototype.ensurePolled = function () { + if (this.pollingFast && this.poll()) { this.pollingFast = false; } + }; + + TextareaInput.prototype.onKeyPress = function () { + if (ie && ie_version >= 9) { this.hasSelection = null; } + this.fastPoll(); + }; + + TextareaInput.prototype.onContextMenu = function (e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + if (input.contextMenuPending) { input.contextMenuPending(); } + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) { return } // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } + + var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; + var wrapperBox = input.wrapper.offsetParent.getBoundingClientRect(); + input.wrapper.style.cssText = "position: static"; + te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + var oldScrollY; + if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) { window.scrollTo(null, oldScrollY); } + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } + input.contextMenuPending = rehide; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + if (input.contextMenuPending != rehide) { return } + input.contextMenuPending = false; + input.wrapper.style.cssText = oldWrapperCSS; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } + var i = 0, poll = function () { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") { + operation(cm, selectAll)(cm); + } else if (i++ < 10) { + display.detectingSelectAll = setTimeout(poll, 500); + } else { + display.selForContextMenu = null; + display.input.reset(); + } + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) { prepareSelectAllHack(); } + if (captureRightClick) { + e_stop(e); + var mouseup = function () { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }; + + TextareaInput.prototype.readOnlyChanged = function (val) { + if (!val) { this.reset(); } + this.textarea.disabled = val == "nocursor"; + this.textarea.readOnly = !!val; + }; + + TextareaInput.prototype.setUneditable = function () {}; + + TextareaInput.prototype.needsContentAttribute = false; + + function fromTextArea(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + { options.tabindex = textarea.tabIndex; } + if (!options.placeholder && textarea.placeholder) + { options.placeholder = textarea.placeholder; } + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + + var realSubmit; + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form; + realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function () { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function (cm) { + cm.save = save; + cm.getTextArea = function () { return textarea; }; + cm.toTextArea = function () { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (!options.leaveSubmitMethodAlone && typeof textarea.form.submit == "function") + { textarea.form.submit = realSubmit; } + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, + options); + return cm + } + + function addLegacyProps(CodeMirror) { + CodeMirror.off = off; + CodeMirror.on = on; + CodeMirror.wheelEventPixels = wheelEventPixels; + CodeMirror.Doc = Doc; + CodeMirror.splitLines = splitLinesAuto; + CodeMirror.countColumn = countColumn; + CodeMirror.findColumn = findColumn; + CodeMirror.isWordChar = isWordCharBasic; + CodeMirror.Pass = Pass; + CodeMirror.signal = signal; + CodeMirror.Line = Line; + CodeMirror.changeEnd = changeEnd; + CodeMirror.scrollbarModel = scrollbarModel; + CodeMirror.Pos = Pos; + CodeMirror.cmpPos = cmp; + CodeMirror.modes = modes; + CodeMirror.mimeModes = mimeModes; + CodeMirror.resolveMode = resolveMode; + CodeMirror.getMode = getMode; + CodeMirror.modeExtensions = modeExtensions; + CodeMirror.extendMode = extendMode; + CodeMirror.copyState = copyState; + CodeMirror.startState = startState; + CodeMirror.innerMode = innerMode; + CodeMirror.commands = commands; + CodeMirror.keyMap = keyMap; + CodeMirror.keyName = keyName; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.lookupKey = lookupKey; + CodeMirror.normalizeKeyMap = normalizeKeyMap; + CodeMirror.StringStream = StringStream; + CodeMirror.SharedTextMarker = SharedTextMarker; + CodeMirror.TextMarker = TextMarker; + CodeMirror.LineWidget = LineWidget; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + CodeMirror.e_stop = e_stop; + CodeMirror.addClass = addClass; + CodeMirror.contains = contains; + CodeMirror.rmClass = rmClass; + CodeMirror.keyNames = keyNames; + } + + // EDITOR CONSTRUCTOR + + defineOptions(CodeMirror); + + addEditorMethods(CodeMirror); + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); + for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + { CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments)} + })(Doc.prototype[prop]); } } + + eventMixin(Doc); + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name/*, mode, …*/) { + if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } + defineMode.apply(this, arguments); + }; + + CodeMirror.defineMIME = defineMIME; + + // Minimal default mode. + CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); + CodeMirror.defineMIME("text/plain", "null"); + + // EXTENSIONS + + CodeMirror.defineExtension = function (name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function (name, func) { + Doc.prototype[name] = func; + }; + + CodeMirror.fromTextArea = fromTextArea; + + addLegacyProps(CodeMirror); + + CodeMirror.version = "5.65.1"; + + return CodeMirror; + +}))); diff --git a/frontend/src/assets/codemirror/css/css.js b/frontend/src/assets/codemirror/css/css.js new file mode 100644 index 00000000..503c48c2 --- /dev/null +++ b/frontend/src/assets/codemirror/css/css.js @@ -0,0 +1,866 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("css", function(config, parserConfig) { + var inline = parserConfig.inline + if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); + + var indentUnit = config.indentUnit, + tokenHooks = parserConfig.tokenHooks, + documentTypes = parserConfig.documentTypes || {}, + mediaTypes = parserConfig.mediaTypes || {}, + mediaFeatures = parserConfig.mediaFeatures || {}, + mediaValueKeywords = parserConfig.mediaValueKeywords || {}, + propertyKeywords = parserConfig.propertyKeywords || {}, + nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, + fontProperties = parserConfig.fontProperties || {}, + counterDescriptors = parserConfig.counterDescriptors || {}, + colorKeywords = parserConfig.colorKeywords || {}, + valueKeywords = parserConfig.valueKeywords || {}, + allowNested = parserConfig.allowNested, + lineComment = parserConfig.lineComment, + supportsAtComponent = parserConfig.supportsAtComponent === true, + highlightNonStandardPropertyKeywords = config.highlightNonStandardPropertyKeywords !== false; + + var type, override; + function ret(style, tp) { type = tp; return style; } + + // Tokenizers + + function tokenBase(stream, state) { + var ch = stream.next(); + if (tokenHooks[ch]) { + var result = tokenHooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == "@") { + stream.eatWhile(/[\w\\\-]/); + return ret("def", stream.current()); + } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { + return ret(null, "compare"); + } else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (ch === "-") { + if (/[\d.]/.test(stream.peek())) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (stream.match(/^-[\w\\\-]*/)) { + stream.eatWhile(/[\w\\\-]/); + if (stream.match(/^\s*:/, false)) + return ret("variable-2", "variable-definition"); + return ret("variable-2", "variable"); + } else if (stream.match(/^\w+-/)) { + return ret("meta", "meta"); + } + } else if (/[,+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { + return ret("qualifier", "qualifier"); + } else if (/[:;{}\[\]\(\)]/.test(ch)) { + return ret(null, ch); + } else if (stream.match(/^[\w-.]+(?=\()/)) { + if (/^(url(-prefix)?|domain|regexp)$/i.test(stream.current())) { + state.tokenize = tokenParenthesized; + } + return ret("variable callee", "variable"); + } else if (/[\w\\\-]/.test(ch)) { + stream.eatWhile(/[\w\\\-]/); + return ret("property", "word"); + } else { + return ret(null, null); + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + if (quote == ")") stream.backUp(1); + break; + } + escaped = !escaped && ch == "\\"; + } + if (ch == quote || !escaped && quote != ")") state.tokenize = null; + return ret("string", "string"); + }; + } + + function tokenParenthesized(stream, state) { + stream.next(); // Must be '(' + if (!stream.match(/^\s*[\"\')]/, false)) + state.tokenize = tokenString(")"); + else + state.tokenize = null; + return ret(null, "("); + } + + // Context management + + function Context(type, indent, prev) { + this.type = type; + this.indent = indent; + this.prev = prev; + } + + function pushContext(state, stream, type, indent) { + state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context); + return type; + } + + function popContext(state) { + if (state.context.prev) + state.context = state.context.prev; + return state.context.type; + } + + function pass(type, stream, state) { + return states[state.context.type](type, stream, state); + } + function popAndPass(type, stream, state, n) { + for (var i = n || 1; i > 0; i--) + state.context = state.context.prev; + return pass(type, stream, state); + } + + // Parser + + function wordAsValue(stream) { + var word = stream.current().toLowerCase(); + if (valueKeywords.hasOwnProperty(word)) + override = "atom"; + else if (colorKeywords.hasOwnProperty(word)) + override = "keyword"; + else + override = "variable"; + } + + var states = {}; + + states.top = function(type, stream, state) { + if (type == "{") { + return pushContext(state, stream, "block"); + } else if (type == "}" && state.context.prev) { + return popContext(state); + } else if (supportsAtComponent && /@component/i.test(type)) { + return pushContext(state, stream, "atComponentBlock"); + } else if (/^@(-moz-)?document$/i.test(type)) { + return pushContext(state, stream, "documentTypes"); + } else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) { + return pushContext(state, stream, "atBlock"); + } else if (/^@(font-face|counter-style)/i.test(type)) { + state.stateArg = type; + return "restricted_atBlock_before"; + } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) { + return "keyframes"; + } else if (type && type.charAt(0) == "@") { + return pushContext(state, stream, "at"); + } else if (type == "hash") { + override = "builtin"; + } else if (type == "word") { + override = "tag"; + } else if (type == "variable-definition") { + return "maybeprop"; + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } else if (type == ":") { + return "pseudo"; + } else if (allowNested && type == "(") { + return pushContext(state, stream, "parens"); + } + return state.context.type; + }; + + states.block = function(type, stream, state) { + if (type == "word") { + var word = stream.current().toLowerCase(); + if (propertyKeywords.hasOwnProperty(word)) { + override = "property"; + return "maybeprop"; + } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) { + override = highlightNonStandardPropertyKeywords ? "string-2" : "property"; + return "maybeprop"; + } else if (allowNested) { + override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag"; + return "block"; + } else { + override += " error"; + return "maybeprop"; + } + } else if (type == "meta") { + return "block"; + } else if (!allowNested && (type == "hash" || type == "qualifier")) { + override = "error"; + return "block"; + } else { + return states.top(type, stream, state); + } + }; + + states.maybeprop = function(type, stream, state) { + if (type == ":") return pushContext(state, stream, "prop"); + return pass(type, stream, state); + }; + + states.prop = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); + if (type == "}" || type == "{") return popAndPass(type, stream, state); + if (type == "(") return pushContext(state, stream, "parens"); + + if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) { + override += " error"; + } else if (type == "word") { + wordAsValue(stream); + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } + return "prop"; + }; + + states.propBlock = function(type, _stream, state) { + if (type == "}") return popContext(state); + if (type == "word") { override = "property"; return "maybeprop"; } + return state.context.type; + }; + + states.parens = function(type, stream, state) { + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == ")") return popContext(state); + if (type == "(") return pushContext(state, stream, "parens"); + if (type == "interpolation") return pushContext(state, stream, "interpolation"); + if (type == "word") wordAsValue(stream); + return "parens"; + }; + + states.pseudo = function(type, stream, state) { + if (type == "meta") return "pseudo"; + + if (type == "word") { + override = "variable-3"; + return state.context.type; + } + return pass(type, stream, state); + }; + + states.documentTypes = function(type, stream, state) { + if (type == "word" && documentTypes.hasOwnProperty(stream.current())) { + override = "tag"; + return state.context.type; + } else { + return states.atBlock(type, stream, state); + } + }; + + states.atBlock = function(type, stream, state) { + if (type == "(") return pushContext(state, stream, "atBlock_parens"); + if (type == "}" || type == ";") return popAndPass(type, stream, state); + if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); + + if (type == "interpolation") return pushContext(state, stream, "interpolation"); + + if (type == "word") { + var word = stream.current().toLowerCase(); + if (word == "only" || word == "not" || word == "and" || word == "or") + override = "keyword"; + else if (mediaTypes.hasOwnProperty(word)) + override = "attribute"; + else if (mediaFeatures.hasOwnProperty(word)) + override = "property"; + else if (mediaValueKeywords.hasOwnProperty(word)) + override = "keyword"; + else if (propertyKeywords.hasOwnProperty(word)) + override = "property"; + else if (nonStandardPropertyKeywords.hasOwnProperty(word)) + override = highlightNonStandardPropertyKeywords ? "string-2" : "property"; + else if (valueKeywords.hasOwnProperty(word)) + override = "atom"; + else if (colorKeywords.hasOwnProperty(word)) + override = "keyword"; + else + override = "error"; + } + return state.context.type; + }; + + states.atComponentBlock = function(type, stream, state) { + if (type == "}") + return popAndPass(type, stream, state); + if (type == "{") + return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false); + if (type == "word") + override = "error"; + return state.context.type; + }; + + states.atBlock_parens = function(type, stream, state) { + if (type == ")") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); + return states.atBlock(type, stream, state); + }; + + states.restricted_atBlock_before = function(type, stream, state) { + if (type == "{") + return pushContext(state, stream, "restricted_atBlock"); + if (type == "word" && state.stateArg == "@counter-style") { + override = "variable"; + return "restricted_atBlock_before"; + } + return pass(type, stream, state); + }; + + states.restricted_atBlock = function(type, stream, state) { + if (type == "}") { + state.stateArg = null; + return popContext(state); + } + if (type == "word") { + if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) || + (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase()))) + override = "error"; + else + override = "property"; + return "maybeprop"; + } + return "restricted_atBlock"; + }; + + states.keyframes = function(type, stream, state) { + if (type == "word") { override = "variable"; return "keyframes"; } + if (type == "{") return pushContext(state, stream, "top"); + return pass(type, stream, state); + }; + + states.at = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == "word") override = "tag"; + else if (type == "hash") override = "builtin"; + return "at"; + }; + + states.interpolation = function(type, stream, state) { + if (type == "}") return popContext(state); + if (type == "{" || type == ";") return popAndPass(type, stream, state); + if (type == "word") override = "variable"; + else if (type != "variable" && type != "(" && type != ")") override = "error"; + return "interpolation"; + }; + + return { + startState: function(base) { + return {tokenize: null, + state: inline ? "block" : "top", + stateArg: null, + context: new Context(inline ? "block" : "top", base || 0, null)}; + }, + + token: function(stream, state) { + if (!state.tokenize && stream.eatSpace()) return null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style && typeof style == "object") { + type = style[1]; + style = style[0]; + } + override = style; + if (type != "comment") + state.state = states[state.state](type, stream, state); + return override; + }, + + indent: function(state, textAfter) { + var cx = state.context, ch = textAfter && textAfter.charAt(0); + var indent = cx.indent; + if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; + if (cx.prev) { + if (ch == "}" && (cx.type == "block" || cx.type == "top" || + cx.type == "interpolation" || cx.type == "restricted_atBlock")) { + // Resume indentation from parent context. + cx = cx.prev; + indent = cx.indent; + } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || + ch == "{" && (cx.type == "at" || cx.type == "atBlock")) { + // Dedent relative to current context. + indent = Math.max(0, cx.indent - indentUnit); + } + } + return indent; + }, + + electricChars: "}", + blockCommentStart: "/*", + blockCommentEnd: "*/", + blockCommentContinue: " * ", + lineComment: lineComment, + fold: "brace" + }; +}); + + function keySet(array) { + var keys = {}; + for (var i = 0; i < array.length; ++i) { + keys[array[i].toLowerCase()] = true; + } + return keys; + } + + var documentTypes_ = [ + "domain", "regexp", "url", "url-prefix" + ], documentTypes = keySet(documentTypes_); + + var mediaTypes_ = [ + "all", "aural", "braille", "handheld", "print", "projection", "screen", + "tty", "tv", "embossed" + ], mediaTypes = keySet(mediaTypes_); + + var mediaFeatures_ = [ + "width", "min-width", "max-width", "height", "min-height", "max-height", + "device-width", "min-device-width", "max-device-width", "device-height", + "min-device-height", "max-device-height", "aspect-ratio", + "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", + "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", + "max-color", "color-index", "min-color-index", "max-color-index", + "monochrome", "min-monochrome", "max-monochrome", "resolution", + "min-resolution", "max-resolution", "scan", "grid", "orientation", + "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", + "pointer", "any-pointer", "hover", "any-hover", "prefers-color-scheme", + "dynamic-range", "video-dynamic-range" + ], mediaFeatures = keySet(mediaFeatures_); + + var mediaValueKeywords_ = [ + "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", + "interlace", "progressive", + "dark", "light", + "standard", "high" + ], mediaValueKeywords = keySet(mediaValueKeywords_); + + var propertyKeywords_ = [ + "align-content", "align-items", "align-self", "alignment-adjust", + "alignment-baseline", "all", "anchor-point", "animation", "animation-delay", + "animation-direction", "animation-duration", "animation-fill-mode", + "animation-iteration-count", "animation-name", "animation-play-state", + "animation-timing-function", "appearance", "azimuth", "backdrop-filter", + "backface-visibility", "background", "background-attachment", + "background-blend-mode", "background-clip", "background-color", + "background-image", "background-origin", "background-position", + "background-position-x", "background-position-y", "background-repeat", + "background-size", "baseline-shift", "binding", "bleed", "block-size", + "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", + "border", "border-bottom", "border-bottom-color", "border-bottom-left-radius", + "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", + "border-collapse", "border-color", "border-image", "border-image-outset", + "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-left", "border-left-color", "border-left-style", + "border-left-width", "border-radius", "border-right", "border-right-color", + "border-right-style", "border-right-width", "border-spacing", "border-style", + "border-top", "border-top-color", "border-top-left-radius", + "border-top-right-radius", "border-top-style", "border-top-width", + "border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing", + "break-after", "break-before", "break-inside", "caption-side", "caret-color", + "clear", "clip", "color", "color-profile", "column-count", "column-fill", + "column-gap", "column-rule", "column-rule-color", "column-rule-style", + "column-rule-width", "column-span", "column-width", "columns", "contain", + "content", "counter-increment", "counter-reset", "crop", "cue", "cue-after", + "cue-before", "cursor", "direction", "display", "dominant-baseline", + "drop-initial-after-adjust", "drop-initial-after-align", + "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size", + "drop-initial-value", "elevation", "empty-cells", "fit", "fit-content", "fit-position", + "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", + "flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into", + "font", "font-family", "font-feature-settings", "font-kerning", + "font-language-override", "font-optical-sizing", "font-size", + "font-size-adjust", "font-stretch", "font-style", "font-synthesis", + "font-variant", "font-variant-alternates", "font-variant-caps", + "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", + "font-variant-position", "font-variation-settings", "font-weight", "gap", + "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", + "grid-column", "grid-column-end", "grid-column-gap", "grid-column-start", + "grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start", + "grid-template", "grid-template-areas", "grid-template-columns", + "grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon", + "image-orientation", "image-rendering", "image-resolution", "inline-box-align", + "inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline", + "inset-inline-end", "inset-inline-start", "isolation", "justify-content", + "justify-items", "justify-self", "left", "letter-spacing", "line-break", + "line-height", "line-height-step", "line-stacking", "line-stacking-ruby", + "line-stacking-shift", "line-stacking-strategy", "list-style", + "list-style-image", "list-style-position", "list-style-type", "margin", + "margin-bottom", "margin-left", "margin-right", "margin-top", "marks", + "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", + "marquee-style", "mask-clip", "mask-composite", "mask-image", "mask-mode", + "mask-origin", "mask-position", "mask-repeat", "mask-size","mask-type", + "max-block-size", "max-height", "max-inline-size", + "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", + "mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", + "nav-up", "object-fit", "object-position", "offset", "offset-anchor", + "offset-distance", "offset-path", "offset-position", "offset-rotate", + "opacity", "order", "orphans", "outline", "outline-color", "outline-offset", + "outline-style", "outline-width", "overflow", "overflow-style", + "overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom", + "padding-left", "padding-right", "padding-top", "page", "page-break-after", + "page-break-before", "page-break-inside", "page-policy", "pause", + "pause-after", "pause-before", "perspective", "perspective-origin", "pitch", + "pitch-range", "place-content", "place-items", "place-self", "play-during", + "position", "presentation-level", "punctuation-trim", "quotes", + "region-break-after", "region-break-before", "region-break-inside", + "region-fragment", "rendering-intent", "resize", "rest", "rest-after", + "rest-before", "richness", "right", "rotate", "rotation", "rotation-point", + "row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span", + "scale", "scroll-behavior", "scroll-margin", "scroll-margin-block", + "scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom", + "scroll-margin-inline", "scroll-margin-inline-end", + "scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right", + "scroll-margin-top", "scroll-padding", "scroll-padding-block", + "scroll-padding-block-end", "scroll-padding-block-start", + "scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end", + "scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right", + "scroll-padding-top", "scroll-snap-align", "scroll-snap-type", + "shape-image-threshold", "shape-inside", "shape-margin", "shape-outside", + "size", "speak", "speak-as", "speak-header", "speak-numeral", + "speak-punctuation", "speech-rate", "stress", "string-set", "tab-size", + "table-layout", "target", "target-name", "target-new", "target-position", + "text-align", "text-align-last", "text-combine-upright", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-skip", + "text-decoration-skip-ink", "text-decoration-style", "text-emphasis", + "text-emphasis-color", "text-emphasis-position", "text-emphasis-style", + "text-height", "text-indent", "text-justify", "text-orientation", + "text-outline", "text-overflow", "text-rendering", "text-shadow", + "text-size-adjust", "text-space-collapse", "text-transform", + "text-underline-position", "text-wrap", "top", "touch-action", "transform", "transform-origin", + "transform-style", "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "translate", + "unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance", + "voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate", + "voice-stress", "voice-volume", "volume", "white-space", "widows", "width", + "will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index", + // SVG-specific + "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", + "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", + "color-interpolation", "color-interpolation-filters", + "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", + "marker", "marker-end", "marker-mid", "marker-start", "paint-order", "shape-rendering", "stroke", + "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", + "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", + "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", + "glyph-orientation-vertical", "text-anchor", "writing-mode", + ], propertyKeywords = keySet(propertyKeywords_); + + var nonStandardPropertyKeywords_ = [ + "accent-color", "aspect-ratio", "border-block", "border-block-color", "border-block-end", + "border-block-end-color", "border-block-end-style", "border-block-end-width", + "border-block-start", "border-block-start-color", "border-block-start-style", + "border-block-start-width", "border-block-style", "border-block-width", + "border-inline", "border-inline-color", "border-inline-end", + "border-inline-end-color", "border-inline-end-style", + "border-inline-end-width", "border-inline-start", "border-inline-start-color", + "border-inline-start-style", "border-inline-start-width", + "border-inline-style", "border-inline-width", "content-visibility", "margin-block", + "margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end", + "margin-inline-start", "overflow-anchor", "overscroll-behavior", "padding-block", "padding-block-end", + "padding-block-start", "padding-inline", "padding-inline-end", + "padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color", + "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color", + "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", + "scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom" + ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_); + + var fontProperties_ = [ + "font-display", "font-family", "src", "unicode-range", "font-variant", + "font-feature-settings", "font-stretch", "font-weight", "font-style" + ], fontProperties = keySet(fontProperties_); + + var counterDescriptors_ = [ + "additive-symbols", "fallback", "negative", "pad", "prefix", "range", + "speak-as", "suffix", "symbols", "system" + ], counterDescriptors = keySet(counterDescriptors_); + + var colorKeywords_ = [ + "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", + "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", + "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", + "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", + "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", + "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", + "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", + "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", + "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", + "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", + "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", + "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", + "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", + "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", + "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", + "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", + "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", + "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", + "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", + "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", + "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", + "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", + "slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", + "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", + "whitesmoke", "yellow", "yellowgreen" + ], colorKeywords = keySet(colorKeywords_); + + var valueKeywords_ = [ + "above", "absolute", "activeborder", "additive", "activecaption", "afar", + "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", + "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", + "arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page", + "avoid-region", "axis-pan", "background", "backwards", "baseline", "below", "bidi-override", "binary", + "bengali", "blink", "block", "block-axis", "blur", "bold", "bolder", "border", "border-box", + "both", "bottom", "break", "break-all", "break-word", "brightness", "bullets", "button", "button-bevel", + "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", + "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", + "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", + "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", + "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse", + "compact", "condensed", "conic-gradient", "contain", "content", "contents", + "content-box", "context-menu", "continuous", "contrast", "copy", "counter", "counters", "cover", "crop", + "cross", "crosshair", "cubic-bezier", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal", + "decimal-leading-zero", "default", "default-button", "dense", "destination-atop", + "destination-in", "destination-out", "destination-over", "devanagari", "difference", + "disc", "discard", "disclosure-closed", "disclosure-open", "document", + "dot-dash", "dot-dot-dash", + "dotted", "double", "down", "drop-shadow", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", + "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", + "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", + "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", + "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", + "ethiopic-halehame-gez", "ethiopic-halehame-om-et", + "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", + "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", + "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fill-box", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", + "forwards", "from", "geometricPrecision", "georgian", "grayscale", "graytext", "grid", "groove", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", + "help", "hidden", "hide", "higher", "highlight", "highlighttext", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "hue-rotate", "icon", "ignore", + "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", + "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", + "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert", + "italic", "japanese-formal", "japanese-informal", "justify", "kannada", + "katakana", "katakana-iroha", "keep-all", "khmer", + "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", + "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten", + "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", + "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", + "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", + "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "manipulation", "match", "matrix", "matrix3d", + "media-controls-background", "media-current-time-display", + "media-fullscreen-button", "media-mute-button", "media-play-button", + "media-return-to-realtime-button", "media-rewind-button", + "media-seek-back-button", "media-seek-forward-button", "media-slider", + "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", + "media-volume-slider-container", "media-volume-sliderthumb", "medium", + "menu", "menulist", "menulist-button", "menulist-text", + "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", + "mix", "mongolian", "monospace", "move", "multiple", "multiple_mask_images", "multiply", "myanmar", "n-resize", + "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", + "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", + "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote", + "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", + "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", + "painted", "page", "paused", "persian", "perspective", "pinch-zoom", "plus-darker", "plus-lighter", + "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", + "progress", "push-button", "radial-gradient", "radio", "read-only", + "read-write", "read-write-plaintext-only", "rectangle", "region", + "relative", "repeat", "repeating-linear-gradient", "repeating-radial-gradient", + "repeating-conic-gradient", "repeat-x", "repeat-y", "reset", "reverse", + "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", + "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running", + "s-resize", "sans-serif", "saturate", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen", + "scroll", "scrollbar", "scroll-position", "se-resize", "searchfield", + "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end", + "semi-condensed", "semi-expanded", "separate", "sepia", "serif", "show", "sidama", + "simp-chinese-formal", "simp-chinese-informal", "single", + "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", + "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", + "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square", + "square-button", "start", "static", "status-bar", "stretch", "stroke", "stroke-box", "sub", + "subpixel-antialiased", "svg_masks", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", + "table-caption", "table-cell", "table-column", "table-column-group", + "table-footer-group", "table-header-group", "table-row", "table-row-group", + "tamil", + "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", + "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", + "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", + "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", + "trad-chinese-formal", "trad-chinese-informal", "transform", + "translate", "translate3d", "translateX", "translateY", "translateZ", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "unidirectional-pan", "unset", "up", + "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", + "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", + "var", "vertical", "vertical-text", "view-box", "visible", "visibleFill", "visiblePainted", + "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", + "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", + "xx-large", "xx-small" + ], valueKeywords = keySet(valueKeywords_); + + var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_) + .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_) + .concat(valueKeywords_); + CodeMirror.registerHelper("hintWords", "css", allWords); + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return ["comment", "comment"]; + } + + CodeMirror.defineMIME("text/css", { + documentTypes: documentTypes, + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + fontProperties: fontProperties, + counterDescriptors: counterDescriptors, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + tokenHooks: { + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + }, + name: "css" + }); + + CodeMirror.defineMIME("text/x-scss", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + lineComment: "//", + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + ":": function(stream) { + if (stream.match(/^\s*\{/, false)) + return [null, null] + return false; + }, + "$": function(stream) { + stream.match(/^[\w-]+/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "#": function(stream) { + if (!stream.eat("{")) return false; + return [null, "interpolation"]; + } + }, + name: "css", + helperType: "scss" + }); + + CodeMirror.defineMIME("text/x-less", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + lineComment: "//", + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + "@": function(stream) { + if (stream.eat("{")) return [null, "interpolation"]; + if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false; + stream.eatWhile(/[\w\\\-]/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "&": function() { + return ["atom", "atom"]; + } + }, + name: "css", + helperType: "less" + }); + + CodeMirror.defineMIME("text/x-gss", { + documentTypes: documentTypes, + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + fontProperties: fontProperties, + counterDescriptors: counterDescriptors, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + supportsAtComponent: true, + tokenHooks: { + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + }, + name: "css", + helperType: "gss" + }); + +}); diff --git a/frontend/src/assets/codemirror/css/gss.html b/frontend/src/assets/codemirror/css/gss.html new file mode 100644 index 00000000..17d24f8a --- /dev/null +++ b/frontend/src/assets/codemirror/css/gss.html @@ -0,0 +1,104 @@ + + +CodeMirror: Closure Stylesheets (GSS) mode + + + + + + + + + + + + + +
+

Closure Stylesheets (GSS) mode

+
+ + +

A mode for Closure Stylesheets (GSS).

+

MIME type defined: text/x-gss.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/frontend/src/assets/codemirror/css/gss_test.js b/frontend/src/assets/codemirror/css/gss_test.js new file mode 100644 index 00000000..2401bc49 --- /dev/null +++ b/frontend/src/assets/codemirror/css/gss_test.js @@ -0,0 +1,17 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function() { + "use strict"; + + var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-gss"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "gss"); } + + MT("atComponent", + "[def @component] {", + "[tag foo] {", + " [property color]: [keyword black];", + "}", + "}"); + +})(); diff --git a/frontend/src/assets/codemirror/css/index.html b/frontend/src/assets/codemirror/css/index.html new file mode 100644 index 00000000..42b327ca --- /dev/null +++ b/frontend/src/assets/codemirror/css/index.html @@ -0,0 +1,81 @@ + + +CodeMirror: CSS mode + + + + + + + + + + + + +
+

CSS mode

+
+ + +

CSS mode supports this option:

+ +
highlightNonStandardPropertyKeywords: boolean
+
Whether to highlight non-standard CSS property keywords such as margin-inline or zoom (default: true).
+
+ +

MIME types defined: text/css, text/x-scss (demo), text/x-less (demo).

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/frontend/src/assets/codemirror/css/less.html b/frontend/src/assets/codemirror/css/less.html new file mode 100644 index 00000000..f7611ef0 --- /dev/null +++ b/frontend/src/assets/codemirror/css/less.html @@ -0,0 +1,152 @@ + + +CodeMirror: LESS mode + + + + + + + + + + +
+

LESS mode

+
+ + +

The LESS mode is a sub-mode of the CSS mode (defined in css.js).

+ +

Parsing/Highlighting Tests: normal, verbose.

+
diff --git a/frontend/src/assets/codemirror/css/less_test.js b/frontend/src/assets/codemirror/css/less_test.js new file mode 100644 index 00000000..abeb6a20 --- /dev/null +++ b/frontend/src/assets/codemirror/css/less_test.js @@ -0,0 +1,54 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function() { + "use strict"; + + var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-less"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "less"); } + + MT("variable", + "[variable-2 @base]: [atom #f04615];", + "[qualifier .class] {", + " [property width]: [variable&callee percentage]([number 0.5]); [comment // returns `50%`]", + " [property color]: [variable&callee saturate]([variable-2 @base], [number 5%]);", + "}"); + + MT("amp", + "[qualifier .child], [qualifier .sibling] {", + " [qualifier .parent] [atom &] {", + " [property color]: [keyword black];", + " }", + " [atom &] + [atom &] {", + " [property color]: [keyword red];", + " }", + "}"); + + MT("mixin", + "[qualifier .mixin] ([variable dark]; [variable-2 @color]) {", + " [property color]: [variable&callee darken]([variable-2 @color], [number 10%]);", + "}", + "[qualifier .mixin] ([variable light]; [variable-2 @color]) {", + " [property color]: [variable&callee lighten]([variable-2 @color], [number 10%]);", + "}", + "[qualifier .mixin] ([variable-2 @_]; [variable-2 @color]) {", + " [property display]: [atom block];", + "}", + "[variable-2 @switch]: [variable light];", + "[qualifier .class] {", + " [qualifier .mixin]([variable-2 @switch]; [atom #888]);", + "}"); + + MT("nest", + "[qualifier .one] {", + " [def @media] ([property width]: [number 400px]) {", + " [property font-size]: [number 1.2em];", + " [def @media] [attribute print] [keyword and] [property color] {", + " [property color]: [keyword blue];", + " }", + " }", + "}"); + + + MT("interpolation", ".@{[variable foo]} { [property font-weight]: [atom bold]; }"); +})(); diff --git a/frontend/src/assets/codemirror/css/scss.html b/frontend/src/assets/codemirror/css/scss.html new file mode 100644 index 00000000..525bab2f --- /dev/null +++ b/frontend/src/assets/codemirror/css/scss.html @@ -0,0 +1,158 @@ + + +CodeMirror: SCSS mode + + + + + + + + + + +
+

SCSS mode

+
+ + +

The SCSS mode is a sub-mode of the CSS mode (defined in css.js).

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/frontend/src/assets/codemirror/css/scss_test.js b/frontend/src/assets/codemirror/css/scss_test.js new file mode 100644 index 00000000..68afc664 --- /dev/null +++ b/frontend/src/assets/codemirror/css/scss_test.js @@ -0,0 +1,110 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-scss"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } + + MT('url_with_quotation', + "[tag foo] { [property background]:[variable&callee url]([string test.jpg]) }"); + + MT('url_with_double_quotes', + "[tag foo] { [property background]:[variable&callee url]([string \"test.jpg\"]) }"); + + MT('url_with_single_quotes', + "[tag foo] { [property background]:[variable&callee url]([string \'test.jpg\']) }"); + + MT('string', + "[def @import] [string \"compass/css3\"]"); + + MT('important_keyword', + "[tag foo] { [property background]:[variable&callee url]([string \'test.jpg\']) [keyword !important] }"); + + MT('variable', + "[variable-2 $blue]:[atom #333]"); + + MT('variable_as_attribute', + "[tag foo] { [property color]:[variable-2 $blue] }"); + + MT('numbers', + "[tag foo] { [property padding]:[number 10px] [number 10] [number 10em] [number 8in] }"); + + MT('number_percentage', + "[tag foo] { [property width]:[number 80%] }"); + + MT('selector', + "[builtin #hello][qualifier .world]{}"); + + MT('singleline_comment', + "[comment // this is a comment]"); + + MT('multiline_comment', + "[comment /*foobar*/]"); + + MT('attribute_with_hyphen', + "[tag foo] { [property font-size]:[number 10px] }"); + + MT('string_after_attribute', + "[tag foo] { [property content]:[string \"::\"] }"); + + MT('directives', + "[def @include] [qualifier .mixin]"); + + MT('basic_structure', + "[tag p] { [property background]:[keyword red]; }"); + + MT('nested_structure', + "[tag p] { [tag a] { [property color]:[keyword red]; } }"); + + MT('mixin', + "[def @mixin] [tag table-base] {}"); + + MT('number_without_semicolon', + "[tag p] {[property width]:[number 12]}", + "[tag a] {[property color]:[keyword red];}"); + + MT('atom_in_nested_block', + "[tag p] { [tag a] { [property color]:[atom #000]; } }"); + + MT('interpolation_in_property', + "[tag foo] { #{[variable-2 $hello]}:[number 2]; }"); + + MT('interpolation_in_selector', + "[tag foo]#{[variable-2 $hello]} { [property color]:[atom #000]; }"); + + MT('interpolation_error', + "[tag foo]#{[variable foo]} { [property color]:[atom #000]; }"); + + MT("divide_operator", + "[tag foo] { [property width]:[number 4] [operator /] [number 2] }"); + + MT('nested_structure_with_id_selector', + "[tag p] { [builtin #hello] { [property color]:[keyword red]; } }"); + + MT('indent_mixin', + "[def @mixin] [tag container] (", + " [variable-2 $a]: [number 10],", + " [variable-2 $b]: [number 10])", + "{}"); + + MT('indent_nested', + "[tag foo] {", + " [tag bar] {", + " }", + "}"); + + MT('indent_parentheses', + "[tag foo] {", + " [property color]: [variable&callee darken]([variable-2 $blue],", + " [number 9%]);", + "}"); + + MT('indent_vardef', + "[variable-2 $name]:", + " [string 'val'];", + "[tag tag] {", + " [tag inner] {", + " [property margin]: [number 3px];", + " }", + "}"); +})(); diff --git a/frontend/src/assets/codemirror/css/test.js b/frontend/src/assets/codemirror/css/test.js new file mode 100644 index 00000000..64352d74 --- /dev/null +++ b/frontend/src/assets/codemirror/css/test.js @@ -0,0 +1,217 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "css"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + // Error, because "foobarhello" is neither a known type or property, but + // property was expected (after "and"), and it should be in parentheses. + MT("atMediaUnknownType", + "[def @media] [attribute screen] [keyword and] [error foobarhello] { }"); + + // Soft error, because "foobarhello" is not a known property or type. + MT("atMediaUnknownProperty", + "[def @media] [attribute screen] [keyword and] ([error foobarhello]) { }"); + + // Make sure nesting works with media queries + MT("atMediaMaxWidthNested", + "[def @media] [attribute screen] [keyword and] ([property max-width]: [number 25px]) { [tag foo] { } }"); + + MT("atMediaFeatureValueKeyword", + "[def @media] ([property orientation]: [keyword landscape]) { }"); + + MT("atMediaUnknownFeatureValueKeyword", + "[def @media] ([property orientation]: [error upsidedown]) { }"); + + MT("atMediaUppercase", + "[def @MEDIA] ([property orienTAtion]: [keyword landScape]) { }"); + + MT("tagSelector", + "[tag foo] { }"); + + MT("classSelector", + "[qualifier .foo-bar_hello] { }"); + + MT("idSelector", + "[builtin #foo] { [error #foo] }"); + + MT("tagSelectorUnclosed", + "[tag foo] { [property margin]: [number 0] } [tag bar] { }"); + + MT("tagStringNoQuotes", + "[tag foo] { [property font-family]: [variable hello] [variable world]; }"); + + MT("tagStringDouble", + "[tag foo] { [property font-family]: [string \"hello world\"]; }"); + + MT("tagStringSingle", + "[tag foo] { [property font-family]: [string 'hello world']; }"); + + MT("tagColorKeyword", + "[tag foo] {", + " [property color]: [keyword black];", + " [property color]: [keyword navy];", + " [property color]: [keyword yellow];", + "}"); + + MT("tagColorHex3", + "[tag foo] { [property background]: [atom #fff]; }"); + + MT("tagColorHex4", + "[tag foo] { [property background]: [atom #ffff]; }"); + + MT("tagColorHex6", + "[tag foo] { [property background]: [atom #ffffff]; }"); + + MT("tagColorHex8", + "[tag foo] { [property background]: [atom #ffffffff]; }"); + + MT("tagColorHex5Invalid", + "[tag foo] { [property background]: [atom&error #fffff]; }"); + + MT("tagColorHexInvalid", + "[tag foo] { [property background]: [atom&error #ffg]; }"); + + MT("tagNegativeNumber", + "[tag foo] { [property margin]: [number -5px]; }"); + + MT("tagPositiveNumber", + "[tag foo] { [property padding]: [number 5px]; }"); + + MT("tagVendor", + "[tag foo] { [meta -foo-][property box-sizing]: [meta -foo-][atom border-box]; }"); + + MT("tagBogusProperty", + "[tag foo] { [property&error barhelloworld]: [number 0]; }"); + + MT("tagTwoProperties", + "[tag foo] { [property margin]: [number 0]; [property padding]: [number 0]; }"); + + MT("tagTwoPropertiesURL", + "[tag foo] { [property background]: [variable&callee url]([string //example.com/foo.png]); [property padding]: [number 0]; }"); + + MT("indent_tagSelector", + "[tag strong], [tag em] {", + " [property background]: [variable&callee rgba](", + " [number 255], [number 255], [number 0], [number .2]", + " );", + "}"); + + MT("indent_atMedia", + "[def @media] {", + " [tag foo] {", + " [property color]:", + " [keyword yellow];", + " }", + "}"); + + MT("indent_comma", + "[tag foo] {", + " [property font-family]: [variable verdana],", + " [atom sans-serif];", + "}"); + + MT("indent_parentheses", + "[tag foo]:[variable-3 before] {", + " [property background]: [variable&callee url](", + "[string blahblah]", + "[string etc]", + "[string ]) [keyword !important];", + "}"); + + MT("font_face", + "[def @font-face] {", + " [property font-family]: [string 'myfont'];", + " [error nonsense]: [string 'abc'];", + " [property src]: [variable&callee url]([string http://blah]),", + " [variable&callee url]([string http://foo]);", + "}"); + + MT("empty_url", + "[def @import] [variable&callee url]() [attribute screen];"); + + MT("parens", + "[qualifier .foo] {", + " [property background-image]: [variable&callee fade]([atom #000], [number 20%]);", + " [property border-image]: [variable&callee linear-gradient](", + " [atom to] [atom bottom],", + " [variable&callee fade]([atom #000], [number 20%]) [number 0%],", + " [variable&callee fade]([atom #000], [number 20%]) [number 100%]", + " );", + "}"); + + MT("css_variable", + ":[variable-3 root] {", + " [variable-2 --main-color]: [atom #06c];", + "}", + "[tag h1][builtin #foo] {", + " [property color]: [variable&callee var]([variable-2 --main-color]);", + "}"); + + MT("blank_css_variable", + ":[variable-3 root] {", + " [variable-2 --]: [atom #06c];", + "}", + "[tag h1][builtin #foo] {", + " [property color]: [variable&callee var]([variable-2 --]);", + "}"); + + MT("supports", + "[def @supports] ([keyword not] (([property text-align-last]: [atom justify]) [keyword or] ([meta -moz-][property text-align-last]: [atom justify])) {", + " [property text-align-last]: [atom justify];", + "}"); + + MT("document", + "[def @document] [variable&callee url]([string http://blah]),", + " [variable&callee url-prefix]([string https://]),", + " [variable&callee domain]([string blah.com]),", + " [variable&callee regexp]([string \".*blah.+\"]) {", + " [builtin #id] {", + " [property background-color]: [keyword white];", + " }", + " [tag foo] {", + " [property font-family]: [variable Verdana], [atom sans-serif];", + " }", + "}"); + + MT("document_url", + "[def @document] [variable&callee url]([string http://blah]) { [qualifier .class] { } }"); + + MT("document_urlPrefix", + "[def @document] [variable&callee url-prefix]([string https://]) { [builtin #id] { } }"); + + MT("document_domain", + "[def @document] [variable&callee domain]([string blah.com]) { [tag foo] { } }"); + + MT("document_regexp", + "[def @document] [variable&callee regexp]([string \".*blah.+\"]) { [builtin #id] { } }"); + + MT("counter-style", + "[def @counter-style] [variable binary] {", + " [property system]: [atom numeric];", + " [property symbols]: [number 0] [number 1];", + " [property suffix]: [string \".\"];", + " [property range]: [atom infinite];", + " [property speak-as]: [atom numeric];", + "}"); + + MT("counter-style-additive-symbols", + "[def @counter-style] [variable simple-roman] {", + " [property system]: [atom additive];", + " [property additive-symbols]: [number 10] [variable X], [number 5] [variable V], [number 1] [variable I];", + " [property range]: [number 1] [number 49];", + "}"); + + MT("counter-style-use", + "[tag ol][qualifier .roman] { [property list-style]: [variable simple-roman]; }"); + + MT("counter-style-symbols", + "[tag ol] { [property list-style]: [variable&callee symbols]([atom cyclic] [string \"*\"] [string \"\\2020\"] [string \"\\2021\"] [string \"\\A7\"]); }"); + + MT("comment-does-not-disrupt", + "[def @font-face] [comment /* foo */] {", + " [property src]: [variable&callee url]([string x]);", + " [property font-family]: [variable One];", + "}") +})(); diff --git a/frontend/src/assets/codemirror/yaml-frontmatter/index.html b/frontend/src/assets/codemirror/yaml-frontmatter/index.html new file mode 100644 index 00000000..f55b5bd8 --- /dev/null +++ b/frontend/src/assets/codemirror/yaml-frontmatter/index.html @@ -0,0 +1,121 @@ + + +CodeMirror: YAML front matter mode + + + + + + + + + + + + + +
+

YAML front matter mode

+
+ +

Defines a mode that parses +a YAML frontmatter +at the start of a file, switching to a base mode at the end of that. +Takes a mode configuration option base to configure the +base mode, which defaults to "gfm".

+ + + +
diff --git a/frontend/src/assets/codemirror/yaml-frontmatter/yaml-frontmatter.js b/frontend/src/assets/codemirror/yaml-frontmatter/yaml-frontmatter.js new file mode 100644 index 00000000..5c6175e4 --- /dev/null +++ b/frontend/src/assets/codemirror/yaml-frontmatter/yaml-frontmatter.js @@ -0,0 +1,72 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function (mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../yaml/yaml")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../yaml/yaml"], mod) + else // Plain browser env + mod(CodeMirror) +})(function (CodeMirror) { + + var START = 0, FRONTMATTER = 1, BODY = 2 + + // a mixed mode for Markdown text with an optional YAML front matter + CodeMirror.defineMode("yaml-frontmatter", function (config, parserConfig) { + var yamlMode = CodeMirror.getMode(config, "yaml") + var innerMode = CodeMirror.getMode(config, parserConfig && parserConfig.base || "gfm") + + function localMode(state) { + return state.state == FRONTMATTER ? {mode: yamlMode, state: state.yaml} : {mode: innerMode, state: state.inner} + } + + return { + startState: function () { + return { + state: START, + yaml: null, + inner: CodeMirror.startState(innerMode) + } + }, + copyState: function (state) { + return { + state: state.state, + yaml: state.yaml && CodeMirror.copyState(yamlMode, state.yaml), + inner: CodeMirror.copyState(innerMode, state.inner) + } + }, + token: function (stream, state) { + if (state.state == START) { + if (stream.match('---', false)) { + state.state = FRONTMATTER + state.yaml = CodeMirror.startState(yamlMode) + return yamlMode.token(stream, state.yaml) + } else { + state.state = BODY + return innerMode.token(stream, state.inner) + } + } else if (state.state == FRONTMATTER) { + var end = stream.sol() && stream.match(/(---|\.\.\.)/, false) + var style = yamlMode.token(stream, state.yaml) + if (end) { + state.state = BODY + state.yaml = null + } + return style + } else { + return innerMode.token(stream, state.inner) + } + }, + innerMode: localMode, + indent: function(state, a, b) { + var m = localMode(state) + return m.mode.indent ? m.mode.indent(m.state, a, b) : CodeMirror.Pass + }, + blankLine: function (state) { + var m = localMode(state) + if (m.mode.blankLine) return m.mode.blankLine(m.state) + } + } + }) +}); diff --git a/frontend/src/assets/codemirror/yaml/index.html b/frontend/src/assets/codemirror/yaml/index.html new file mode 100644 index 00000000..6014d9d3 --- /dev/null +++ b/frontend/src/assets/codemirror/yaml/index.html @@ -0,0 +1,80 @@ + + +CodeMirror: YAML mode + + + + + + + + + +
+

YAML mode

+
+ + +

MIME types defined: text/x-yaml.

+ +
diff --git a/frontend/src/assets/codemirror/yaml/yaml.js b/frontend/src/assets/codemirror/yaml/yaml.js new file mode 100644 index 00000000..d4649410 --- /dev/null +++ b/frontend/src/assets/codemirror/yaml/yaml.js @@ -0,0 +1,120 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("yaml", function() { + + var cons = ['true', 'false', 'on', 'off', 'yes', 'no']; + var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i'); + + return { + token: function(stream, state) { + var ch = stream.peek(); + var esc = state.escaped; + state.escaped = false; + /* comments */ + if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) { + stream.skipToEnd(); + return "comment"; + } + + if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) + return "string"; + + if (state.literal && stream.indentation() > state.keyCol) { + stream.skipToEnd(); return "string"; + } else if (state.literal) { state.literal = false; } + if (stream.sol()) { + state.keyCol = 0; + state.pair = false; + state.pairStart = false; + /* document start */ + if(stream.match('---')) { return "def"; } + /* document end */ + if (stream.match('...')) { return "def"; } + /* array list item */ + if (stream.match(/\s*-\s+/)) { return 'meta'; } + } + /* inline pairs/lists */ + if (stream.match(/^(\{|\}|\[|\])/)) { + if (ch == '{') + state.inlinePairs++; + else if (ch == '}') + state.inlinePairs--; + else if (ch == '[') + state.inlineList++; + else + state.inlineList--; + return 'meta'; + } + + /* list separator */ + if (state.inlineList > 0 && !esc && ch == ',') { + stream.next(); + return 'meta'; + } + /* pairs separator */ + if (state.inlinePairs > 0 && !esc && ch == ',') { + state.keyCol = 0; + state.pair = false; + state.pairStart = false; + stream.next(); + return 'meta'; + } + + /* start of value of a pair */ + if (state.pairStart) { + /* block literals */ + if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; }; + /* references */ + if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; } + /* numbers */ + if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; } + if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; } + /* keywords */ + if (stream.match(keywordRegex)) { return 'keyword'; } + } + + /* pairs (associative arrays) -> key */ + if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) { + state.pair = true; + state.keyCol = stream.indentation(); + return "atom"; + } + if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; } + + /* nothing found, continue */ + state.pairStart = false; + state.escaped = (ch == '\\'); + stream.next(); + return null; + }, + startState: function() { + return { + pair: false, + pairStart: false, + keyCol: 0, + inlinePairs: 0, + inlineList: 0, + literal: false, + escaped: false + }; + }, + lineComment: "#", + fold: "indent" + }; +}); + +CodeMirror.defineMIME("text/x-yaml", "yaml"); +CodeMirror.defineMIME("text/yaml", "yaml"); + +}); diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json new file mode 100644 index 00000000..b88a4c05 --- /dev/null +++ b/frontend/src/assets/i18n/en.json @@ -0,0 +1,439 @@ +{ + "validator": { + "empty": "This field is required.", + "email": "Please enter the correct email address", + "number": "Please enter numbers", + "word": "Can not contain special symbols", + "engu": "Please enter numbers and letters", + "internet": "Invalid URL: http:// or https:// schema is required", + "ip": "Please enter the correct IP", + "zeroToHundred": "Please enter a number between 0 and 100 (including 0 and 100)", + "noSpace":"Input can not contain space", + "json":"Invaild JSON format", + "fqdn":"Invaild domain name", + "match":"Passwords do not match.", + "password":"Password should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.", + "fileSize": "The maximum upload file size is d%", + "fileFormat": "The upload file format can only be: d%" + }, + "serverMessage": { + "default200": "Successful operation", + "default401": "Please log in.", + "default404": "Error 404: request failed.", + "delete200": "Delete successfully", + "create200": "Create successfully", + "upload200": "Upload successfully", + "download200": "Download successfully", + "update200": "Update successfully", + "check": "Check successfully", + "connect": "Connect successfully", + "scan": "Scan successfully", + "login": "Login successfully", + "logout": "Logout successfully", + "update_password_success": "Password is successfully updated!", + "login_again_alert":"The page will redirect to login page in 3s." + }, + "CommonlyUse": { + "name": "Name", + "ip": "IP Address", + "port": "HTTP Port", + "Port": "Port", + "unregister": "Unregister", + "register": "Register", + "test": "test", + "cancel" : "Cancel", + "Cancel" : "CANCEL", + "add": "ADD", + "modify": "Modify", + "Create": "CREATE", + "save": "Save", + "few": "At least ", + "character": " characters", + "many": "Up to ", + "description": "Description", + "all": "All", + "detail": "Detail", + "createTime": "Create Time", + "creationTime": "Creation Time", + "updateTime": "Update Time", + "action": "Action", + "actions": "Actions", + "closed": "Closed", + "close": "Close", + "siteName": "Site Name", + "back": "Back", + "refresh": "Refresh", + "id": "ID", + "status": "Status", + "select": "Select", + "version": "Version", + "ok": "Ok", + "joined": "Joined", + "pending": "Pending", + "delete": "Delete", + "yes": "YES", + "uploading": "Uploading", + "upload": "Upload", + "reset": "Reset", + "submit": "Submit", + "creating": "Creating", + "download":"Download", + "success":"Success", + "file":"Python Files", + "project":"project", + "accept": "Accept", + "decline": "Decline", + "saving": "Saving", + "error":"Error", + "user":"user", + "deleting":"Deleting", + "model":"model", + "item":"item(s)", + "null": "N/A", + "true" : "True", + "false" : "False", + "leaving":"Leaving", + "pleasewait": "Please wait", + "type": "Type", + "next": "Next", + "installation": "Installation", + "notAdd": "Not Add", + "new": "New", + "check": "Check", + "submitting": "Submitting", + "update": "Update", + "edit" : "Edit", + "viewDetail": "View Detail", + "finish": "Finish", + "skip": "Skip", + "overview": "Overview", + "expirationDate": "Expiration Date", + "filter": "Filter", + "requirements": "(Optional) requirements.txt", + "editorModeHelperMessage":"Click text area to enter editor mode" + }, + "EndpointMg": { + "name": "Endpoint", + "infraUUID": "Infra UUID", + "infraName": "Infra Name", + "endpointURL": "Endpoint URL", + "deleteEndpoint": "Do you want to delete this Endpoint?" + }, + "EndpointNew": { + "newEndpoint": "Add a New Endpoint", + "selectInfra": "Select a Infrastructure", + "currentInfra": "Current Infra Selection", + "endpointConfiguration": "Endpoint Configuration", + "endpointType": "Endpoint Type", + "currentSelection": "Current Selection", + "notHasBeenlifecycleManager": "This endpoint has been installed on d%, but hasn't been added to Lifecycle Manager yet.", + "hasBeenlifecycleManager": "This endpoint has been installed on d% and added to Lifecycle Manager.", + "youWantAdd": "Do you want to add it to Lifecycle Manager?", + "add": "Add it", + "reinstall": "Reinstall", + "currentEndpoint": "Current Endpoint Selection", + "getKubeFATEInstallation": "Get KubeFATE Installation YAML", + "addEndpoint": "Add Endpoint", + "noIinfra": "No Infrastructure available.", + "clickHere": "Click Here to add.", + "hostname" :"Hostname", + "serviceUsername": "Service Username", + "servicePassword": "Service Password", + "ingressControllerService": "Install an Ingress Controller for me", + "notCompatible": "This endpoint is not compatible, please manually uninstall it before proceeding.", + "registryConfiguration": "Registry Configuration", + "serviceConfiguration": "Service Configuration" + }, + "EndpointDetail": { + "name": "Endpoint Detail", + "infraProviderName": "Infrastructure Name", + "infraProviderUUID": "Infrastructure UUID", + "YAML": "KubeFATE Deployment YAML", + "host": "Host", + "checkKubefate": "Check Status", + "removeKubeFATEInstallation": "Also remove the KubeFATE installation" + }, + "InfraProvider": { + "name": "Infrastructure", + "server": "Server", + "APIServer": "API Server", + "add": "Add a New Infrastructure", + "kubeconfig": "Kubeconfig", + "detail": "Infrastructure Detail", + "information": "Kubernetes Provider Information", + "isDelete": "Do you want to delete this Infrastructure?", + "update": "Update Infrastructure Configuration", + "wantDelete": "Do you want to delete", + "below": "Infrastructure(s) listed below?", + "configuration": "FATE Registry Configuration", + "useRegistryConfiguration": "Use Registry Configuration", + "configureRegistry": "Configure Registry", + "registry": "Registry", + "imageName": "The final image name will be like", + "useRegistrySecret": "Use Registry Secret", + "serverURL": "Server URL", + "username": "Username", + "password": "Password", + "toSubmit": "To submit, you need to pass the Kubeconfig test.", + "urlSuggestion": "You may want to use:", + "togglePlacerHolder": "Toggle to 'false' to use the deafult registry from Docker Hub" + }, + "Federation": { + "name": "Federation", + "federationName": "Federation Name", + "add": "Add a New Federation", + "domain": "Domain", + "domainExplain": "All deployed FATE services will use subdomains based on this domain field", + "isDelete": "Do you want to delete this federation?", + "customize": "Customize shard descriptor", + "sample": "Sample shape", + "target": "Target shape", + "commas": "Please separate with commas", + "pythonFiles": "Python Files", + "envoyConfig": "Envoy Config" + }, + "FederationDetail": { + "name": "FATE Federation Detail", + "exchangeName": "Exchange Name", + "isDelete": "Do you want to delete this ", + "cluster": "Cluster", + "clusterName": "Cluster Name", + "partyId": "Party ID", + "exchange": "Exchange", + "noexchange": "No exchange, please add one.", + "noactiveexchange": "You should have an active exchange to create cluster.", + "accessInfo": "Exposed Services", + "forceRemove": "Force remove" + }, + "NewCluster": { + "name": "Create a New Cluster", + "basicInformation": "Basic Information", + "selectEndpoint": "Select an Endpoint", + "noEndpoint": "No endpoint available.", + "selectChart": "Select a Chart", + "setPartyID": "Set a Party ID", + "setNamespace": "Set a Namespace", + "namespace": "Namespace", + "selectCertificates": "Select Certificates", + "useCertificates": "Install certificates for me", + "skip": "Skip, I will manually install certificates", + "pulsarServerCertificate": "Pulsar Server Certificate", + "addNew": "Generate new", + "useExist": "Use existing (Coming soon)", + "client": "Client", + "server": "Server", + "certificate": "Site-Portal Certificate", + "checkYAML": "Final Check", + "getYAML": "Get YAML", + "volumeConfig": "Volume Configuration", + "enablePersistence": "Enable Volume Persistence", + "storageClassName": "Storage Class Name", + "serviceType": "Service Type", + "loadBalancer": "Load Balancer", + "nodePort": "NodePort", + "new": "Create a new one", + "import": "Import an external cluster", + "externalClusterAccessInfo": "External Cluster Access Info", + "pulsar": "Pulsar Public TLS", + "nginx" : "Nginx", + "host" : "Host", + "port" : "Port" + }, + "ExchangeNew": { + "name": "Create a New Exchange", + "certificate": "FML Manager Certificate", + "proxyServerCertificate":"Proxy Server Certificate", + "chooseServiceType": "Choose Service Type", + "serviceType": "Service Type", + "loadBalancer": "Load Balancer", + "nodePort": "NodePort", + "new": "Create a new one", + "import": "Import an external exchange", + "externalExchangeAccessInfo": "External Exchange Access Info", + "trafficServer": "Traffic Server", + "nginx" : "Nginx", + "host" : "Host", + "port" : "Port" + + }, + "ExchangeDetail": { + "detail": "Exchange Detail", + "accessInfo": "Exposed Services", + "fqdn": "FQDN", + "serviceType": "Service Type", + "tls": "TLS", + "clusterUuid": "Cluster UUID in KubeFATE", + "infraProviderName": "Infrastructure Name", + "infraProviderUuid": "Infrastructure UUID", + "endpointName": "Endpoint Name", + "endpointUuid": "Endpoint UUID", + "deploymentYaml": "Deployment YAML", + "fmlManagerClientCertInfo": "FML Manager Client Certificate", + "fmlManagerServerCertInfo": "FML Manager Server Certificate", + "proxyServerCertInfo": "Proxy Server Certificate", + "bindingMode": "Binding Mode", + "commonName": "Common Name", + "noAccessInformation" :"No access info", + "certificateInformation": "Certificate Information", + "isDelete": "Do you want to delete this Exchange?" + }, + "ClusterDetail":{ + "detail": "Cluster Detail", + "partyId": "Party ID", + "pulsarServerCertInfo": "Pulsar Server Certificate", + "sitePortalClientCertInfo": "Site Portal Client Certificate", + "sitePortalServerCertInfo": "Site Portal Server Certificate", + "isDelete": "Do you want to delete this Cluster?", + "ingressInfo": "Ingress Info", + "addresses": "Addresses" + }, + "Chart": { + "name": "Chart", + "helmChartName": "Helm Chart Name", + "helmChartVersion": "Helm Chart Version", + "add": "Add a New Chart" + }, + "ChartDetail": { + "name": "Chart Detail", + "values": "Values", + "template": "Values Template", + "about": "About" + }, + "Certificate": { + "name": "Certificate", + "names": "Certificates", + "authority": "Certificate Authority", + "commonName": "Common Name", + "expirationDate": "Expiration Date", + "serialNumber": "Serial Number", + "bindings": "Bindings", + "seriesNumber": "Series Number", + "expiredDate": "Expired Date", + "bindingServer": "Binding Server", + "add": "Add a New Certificate", + "startDate": "Start Date", + "endDate": "End Date", + "isDelete": "Do you want to delete the certificate(s) below?", + "noCertificateAuthority": "No certificate authority, please add one." + }, + "CertificateDetail": { + "name": "Certificate Detail", + "commonName": "Common Name", + "expirationDate": "Expiration Date", + "serialNumber": "Serial Number", + "bind": "Bindings", + "participantUuid": "Participant UUID", + "participantName": "Participant Name", + "serviceDescription": "Service Description", + "serviceType": "Service Type", + "serviceURL": "Service URL", + "provisionerName": "Provisioner Name", + "provisionerPassword": "Provisioner Password", + "stepCA": "StepCA", + "pem": "Root certificate in PEM format", + "statusErrorAlert": "Please check and update your CA configuration. Below is the problem detail:", + "autoFill":"Auto fill with built-in StepCA config", + "manually": "Set up StepCA config manually", + "choose": "You can choose:", + "federationType": "Federation Type", + "federationName": "Federation Name" + }, + "Header": { + "lifecycleManager": "Lifecycle Manager", + "logOut": "Log Out", + "curPassword": "Current Password", + "newPassword": "New Password", + "changePassword": "Change Password", + "confirmPassword": "Confirm Password" + }, + "Nav": { + "federation": "Federation", + "infraProvider": "Infrastructure", + "endpoint": "Endpoint", + "chart": "Chart", + "certificate": "Certificate" + }, + "Event": { + "event":"Event", + "entityType": "Entity Type", + "logLevel": "Log Level" + }, +"FederationOpenFlDetail": { + "name": "OpenFL Federation Detail", + "directorName": "Director Name", + "director": "Server (Director)", + "client": "Client (Envoy)", + "nodirector": "No director, please add one.", + "nodirectorToken": "You should have an active director to registration token.", + "envoy": "Envoy Instance", + "token": "Registration Token", + "token_name": "Token Name", + "limit": "Limit", + "labels": "Labels", + "addToken": "Add a New Token", + "multipleDeletion": "Multiple Deletion", + "multipleEnvoysDeletionMessage": "Do you want to delete envoy(s) listed below?" +}, +"DirectorNew": { + "name": "Create a New Director", + "files": "Selected file(s):", + "directorServerCertificate":"Director Server Certificate", + "certificate": "Director API Client Certificate", + "jupyterNotebook": "Jupyter Notebook Configuration", + "password": "Access Password", + "configuration": "Registry Configuration", + "selectFile:": "Selected file(s):" +}, +"DirectorDetail": { + "detail": "Director Detail", + "directorServerCertInfo": "Director Server Certificate", + "jupyterClientCertInfo":"Jupyter Client Certificate" + +}, +"EnvoyDetail":{ + "detail": "Client Detail", + "isDelete": "Do you want to delete this Client?", + "envoyClientCertInfo": "Envoy Client Certificate" +}, +"PSP":{ + "pspConfigTitle": "Pod Security Policy Configuration", + "enablePSP": "Enable Pod Security Policy:" +}, +"Openfl":{ + "pythonFileUploadErrorMessage": "Error: Invaild file type: Python script file with '.py' suffix required.", + "requirementFileUploadErrorMessage": "Error: Invaild file type:requirements.txt required." +}, + "Endpoint": "Endpoint", + "Exchange": "Exchange", + "Cluster": "Cluster", + "LogMessage": "Log Message", + "KubeFATE":"KubeFATE", + "Unknown": "Unknown", + "Creating": "Creating", + "Ready": "Ready", + "Dismissed": "Dismissed", + "Unavailable": "Unavailable", + "Deleting": "Deleting", + "Active": "Active", + "Installing":"Installing", + "Failed": "Failed", + "Removing": "Removing", + "FATEExchange": "FATE Exchange", + "FATECluster": "FATE Cluster", + "OpenFLDirector": "OpenFL Director", + "OpenFLEnvoy": "OpenFL Envoy", + "StepCA":"StepCA", + "PulsarServer":"Pulsar Server", + "Healthy":"Healthy", + "Unhealthy":"Unhealthy", + "en": "English", + "zh_CN": "中文 (简体)", + "success": "Success.", + "new": "Generate new certificate", + "existing": "Use existing", + "InstallingDirector": "Installing Director", + "ConfiguringInfra": "Configuring Infra", + "InstallingEndpoint": "Installing Endpoint", + "ConfiguringEndpoint": "Configuring Endpoint", + "InstallingEnvoy": "Installing Envoy" +} \ No newline at end of file diff --git a/frontend/src/assets/i18n/zh_CN.json b/frontend/src/assets/i18n/zh_CN.json new file mode 100644 index 00000000..1cfa67e1 --- /dev/null +++ b/frontend/src/assets/i18n/zh_CN.json @@ -0,0 +1,440 @@ +{ + "validator": { + "empty": "当前内容是必填项", + "email": "请输入正确的邮箱地址", + "number": "请输入数字", + "word": "不允许包含特殊符号", + "engu": "只能输入数字与字母", + "internet": "URL格式不合理: 请用http:// 或者 https:// 的格式", + "ip": "请输入正确的IP地址", + "zeroToHundred": "请输入0至100之间的数(包含0和100)", + "noSpace":"输入不能有空格", + "json":"不规范的JSON格式", + "fqdn":"不规范的域名格式", + "match":"两次密码不相同", + "password":"新密码长度必须在8-20之间,至少有一个大写字母,一个小写字母,和一个数字", + "fileSize": "文件最大容量是d%", + "fileFormat": "文件格式只能为:d%" + }, + "serverMessage": { + "default200": "操作成功", + "default401": "当前登录信息已失效,请重新登录!", + "default404": "错误404: 请求失败", + "delete200": "删除成功", + "create200": "创建成功", + "upload200": "上传成功", + "download200": "下载成功", + "update200": "更新成功", + "check": "状态正常", + "connect": "连接成功", + "scan": "扫描成功", + "login": "登录成功", + "logout": "成功退出", + "update_password_success": "密码修改成功!", + "login_again_alert":"页面将于3秒后跳转到登录界面。" + }, + "CommonlyUse": { + "name": "名称", + "ip": "IP 地址", + "port": "端口", + "Port": "端口", + "unregister": "注销", + "register": "登记", + "test": "测试", + "cancel" : "取消", + "Cancel" : "取消", + "add": "添加", + "modify": "修改", + "Create": "创建", + "save": "保存", + "few": "至少 ", + "character": " 字符", + "many": "最多 ", + "description": "描述", + "all": "全部", + "detail": "详情", + "createTime": "创建时间", + "creationTime": "创建时间", + "updateTime": "更新时间", + "action": "操作", + "actions": "操作", + "closed": "关闭", + "close": "关闭", + "siteName": "站点名称", + "back": "返回", + "refresh": "刷新", + "id": "ID", + "status": "状态", + "select": "选择", + "version": "版本", + "ok": "确定", + "joined": "已加入", + "pending": "等待中", + "delete": "删除", + "yes": "确定", + "uploading": "上传中", + "upload": "上传", + "reset": "重置", + "submit": "提交", + "creating": "创建中", + "download":"下载", + "success":"成功", + "file":"Python文件", + "project":"项目", + "accept": "接受", + "decline": "拒绝", + "saving": "保存", + "error":"错误", + "user":"用户", + "deleting":"删除中", + "model":"模型", + "item":"项", + "null": "未知", + "true" : "是", + "false" : "否", + "leaving":"离开中", + "pleasewait": "请等待", + "type": "类型", + "next": "下一步", + "installation": "安装", + "notAdd": "不添加", + "new": "新建", + "check": "检查", + "submitting": "提交中", + "update": "更新", + "edit" : "编辑", + "viewDetail": "查看详情", + "finish": "完成", + "skip": "跳过", + "overview": "概况", + "expirationDate": "到期时间", + "filter": "筛选", + "requirements": "(可选) requirements.txt 文件", + "editorModeHelperMessage":"点击文本区域进入编辑器模式" + }, + "EndpointMg": { + "name": "服务端点", + "infraUUID": "基础设施UUID", + "infraName": "基础设施名称", + "endpointURL": "服务端点URL", + "deleteEndpoint": "确定要删除这个服务端点?" + }, + "EndpointNew": { + "newEndpoint": "新建服务端点", + "selectInfra": "选择一个基础设施提供程序", + "currentInfra": "当前服务端点选择", + "endpointConfiguration": "服务端点配置", + "endpointType": "服务端点类型", + "currentSelection": "当前选择", + "notHasBeenlifecycleManager": "当前服务端点已安装在d%上,但尚未添加到Lifecycle Manager", + "hasBeenlifecycleManager": "当前服务端点已安装在d%上,并已添加到Lifecycle Manager。", + "youWantAdd": "是否要将其添加到Lifecycle Manager?", + "add": "添加它", + "reinstall": "重新安装", + "currentEndpoint": "当前服务端点选择", + "getKubeFATEInstallation": "获取KubeFATE安装YAML", + "addEndpoint": "添加服务端点", + "noIinfra": "没有可用的基础设施提供商", + "clickHere": "点击此处添加", + "hostname" :"主机名称", + "serviceUsername": "服务用户名", + "servicePassword": "服务密码", + "ingressControllerService": "为我安装Ingress控制器", + "notCompatible": "这个服务端点不兼容,请手动卸载后再继续。", + "registryConfiguration": "Registry 配置", + "serviceConfiguration": "服务配置" + }, + "EndpointDetail": { + "name": "服务端点详情", + "infraProviderName": "基础设施提供商", + "infraProviderUUID": "基础设施提供商 UUID", + "YAML": "KubeFATE部署YAML", + "host": "主机", + "checkKubefate": "检查状态", + "removeKubeFATEInstallation": "同时卸载KubeFATE" + }, + "InfraProvider": { + "name": "基础设施", + "server": "服务", + "APIServer": "API 服务", + "add": "新建基础设施", + "kubeconfig": "Kubeconfig", + "detail": "基础设施详情", + "information": "Kubernetes供应商信息", + "isDelete": "确定删除这个基础设施?", + "update": "更新基础设施配置", + "wantDelete": "确定删除以下", + "below": "个基础设施提供商?", + "configuration": "FATE Registry配置", + "configureRegistry": "配置Registry", + "registry": "Registry", + "imageName": "最终的镜像名称如下", + "useRegistrySecret": "使用Registry Secret", + "useRegistryConfiguration": "使用Registry配置", + "serverURL": "服务地址", + "username": "用户名", + "password": "密码", + "toSubmit": "你需要先通过Kubeconfig测试后才能提交。", + "urlSuggestion": "你或许想用:", + "togglePlacerHolder": "将按钮置为'false' 来使用默认的DockerHub Registry" + }, + "Federation": { + "name": "联邦", + "federationName": "联邦名称", + "add": "新建联邦", + "domain": "主域名", + "domainExplain": "各服务的子域名(包括证书信息)将基于此主域名生成", + "isDelete": "确定删除这个联邦?", + "customize": "自定义碎片描述", + "sample": "样本数据Shape", + "target": "目标数据Shape", + "commas": "请使用 ',' 隔开", + "pythonFiles": "Python文件", + "envoyConfig": "Envoy配置文件" + }, + "FederationDetail": { + "name": "FATE联邦详情", + "exchangeName": "Exchange名称", + "isDelete": "确定删除", + "cluster": "Cluster", + "clusterName": "Cluster名称", + "partyId": "组织ID", + "exchange": "Exchange", + "noexchange": "没有Exchange, 请添加一个", + "noactiveexchange": "你应该有一个活动的Exchange来创建群集", + "accessInfo": "公开服务", + "forceRemove": "强制移除" + }, + "NewCluster": { + "name": "创建Cluster", + "basicInformation": "基本信息", + "selectEndpoint": "选择服务端点", + "noEndpoint": "没有可用的服务端点", + "selectChart": "选择一个Chart", + "setPartyID": "设置一个Party ID", + "setNamespace": "设置命名空间", + "namespace": "命名空间", + "selectCertificates": "选择证书", + "useCertificates": "为我安装证书", + "skip": "跳过,我将手动安装证书", + "pulsarServerCertificate": "Pulsar服务器证书", + "addNew": "创建新的", + "useExist": "使用现有的 (即将发布)", + "client": "客户端", + "server": "服务器", + "certificate": "Site-Portal 证书", + "checkYAML": "最后检查", + "getYAML": "获取YAML", + "volumeConfig": "Volume(卷) 配置", + "enablePersistence": "使用持久化卷(Persistent volume)", + "storageClassName": "Storage Class Name", + "serviceType": "服务类型", + "loadBalancer": "负载均衡器", + "nodePort": "节点端口", + "new": "创建一个新的Cluster", + "import": "导入一个外部的Cluster", + "externalClusterAccessInfo": "外部Cluster公开服务", + "pulsar": "Pulsar Public TLS", + "nginx" : "Nginx", + "host" : "Host", + "port" : "Port" + }, + "ExchangeNew": { + "name": "创建Exchange", + "certificate": "FML Manager 证书", + "proxyServerCertificate":"代理服务器证书", + "chooseServiceType": "选择服务类型", + "serviceType": "服务类型", + "loadBalancer": "负载均衡器", + "nodePort": "节点端口", + "new": "创建一个新的Exchange", + "import": "导入一个外部的Exchange", + "externalExchangeAccessInfo": "外部Exchange公开服务", + "trafficServer": "Traffic Server", + "nginx" : "Nginx", + "host" : "Host", + "port" : "Port" + }, + "ExchangeDetail": { + "detail": "Exchange 详情", + "accessInfo": "公开服务", + "fqdn": "FQDN", + "serviceType": "服务类型", + "tls": "TLS", + "clusterUuid": "在KubeFATE里的Cluster UUID", + "infraProviderName": "基础设施名称", + "infraProviderUuid": "基础设施UUID", + "endpointName": "服务端点名称", + "endpointUuid": "服务端点UUID", + "deploymentYaml": "部署YAML", + "fmlManagerClientCertInfo": "FML Manager客户端证书", + "fmlManagerServerCertInfo": "FML Manager服务器证书", + "proxyServerCertInfo": "代理服务器证书", + "bindingMode": "证书绑定模式", + "commonName": "通用名称", + "noAccessInformation" :"无访问信息", + "certificateInformation": "证书信息", + "isDelete": "确定删除这个Exchange?" + + }, + "ClusterDetail":{ + "detail": "Cluster详情", + "partyId": "Party ID", + "pulsarServerCertMode": "Pulsar服务器证书模式", + "pulsarServerCertInfo": "Pulsar服务器证书", + "sitePortalClientCertInfo": "Site Porta客户端证书", + "sitePortalServerCertInfo": "Site Portal服务器证书", + "isDelete": "确定删除这个Cluster?", + "ingressInfo": "Ingress信息", + "addresses": "地址" + }, + "Chart": { + "name": "Chart", + "helmChartName": "Helm Chart名称", + "helmChartVersion": "Helm Chart版本", + "add": "创建Chart" + }, + "ChartDetail": { + "name": "Chart详情", + "values": "Values", + "template": "Values Template", + "about": "关于" + }, + "Certificate": { + "name": "证书", + "names": "证书", + "authority": "证书颁发机构", + "commonName": "通用名称", + "expirationDate": "到期时间", + "serialNumber": "序列号", + "bindings": "绑定", + "seriesNumber": "序列号", + "expiredDate": "到期时间", + "bindingServer": "绑定服务器", + "add": "添加新证书", + "startDate": "开始日期", + "endDate": "截止日期", + "isDelete": "确定删除下面的证书吗?", + "noCertificateAuthority": "没有证书颁发机构,请添加一个" + }, + "CertificateDetail": { + "name": "证书详情", + "serviceURL": "服务URL", + "commonName": "通用名称", + "expirationDate": "到期时间", + "serialNumber": "序列号", + "bind": "绑定", + "participantUuid": "参与者UUID", + "participantName": "参与者名称", + "serviceDescription": "服务描述", + "serviceType": "服务类型", + "provisionerName": "Provisioner名称", + "provisionerPassword": "Provisioner密码", + "stepCA": "StepCA", + "pem": "PEM格式的根证书", + "statusErrorAlert": "请检查并更新你的CA配置。下面是问题的详情:", + "autoFill":"自动填充内置的StepCA配置", + "manually": "手动配置StepCA", + "choose": "你可以选择:", + "federationType": "联邦类型", + "federationName": "联邦名称" + }, + "Header": { + "lifecycleManager": "Lifecycle Manager", + "logOut": "登出", + "curPassword": "当前密码", + "newPassword": "新密码", + "changePassword": "修改密码", + "confirmPassword": "确认密码" + }, + "Nav": { + "federation": "联邦", + "infraProvider": "基础设施", + "endpoint": "服务端点", + "chart": "Chart", + "certificate": "证书" + }, + "Event": { + "event":"事件", + "entityType": "实体类型", + "logLevel": "日志级别" + }, + "FederationOpenFlDetail": { + "name": "OpenFL 联邦详情", + "directorName": "Director 名称", + "director": "服务器 (Director)", + "client": "客户端 (Envoy)", + "nodirector": "没有Director, 请添加一个", + "nodirectorToken": "应该有一个成功运行的Director,再创建注册Token", + "token": "注册Token", + "token_name": "Token名称", + "envoy": "Envoy 实例", + "limit": "次数限制", + "labels": "标签", + "addToken": "添加一个新的Token", + "multipleDeletion": "多项删除", + "multipleEnvoysDeletionMessage": "你想要删除以下的Envoy吗?" + }, + "DirectorNew": { + "name": "创建Director", + "files": "文件列表:", + "directorServerCertificate":"Director服务器证书", + "certificate": "Director API 客户端证书", + "jupyterNotebook": "Jupyter Notebook配置", + "password": "进入密码", + "configuration": "Registry配置", + "selectFile:": "文件列表:" + }, + "DirectorDetail": { + "detail": "Director 详情", + "directorServerCertInfo": "Director 服务器证书", + "jupyterClientCertInfo":"Jupyter 客户端证书" + }, + "EnvoyDetail":{ + "detail": "客户端详情", + "isDelete": "你想要删除这个客户端吗?", + "envoyClientCertInfo": "Envoy 客户端证书" + }, + "PSP":{ + "pspConfigTitle": "配置容器安全策略", + "enablePSP": "启用容器安全策略:" + }, + "Openfl":{ + "pythonFileUploadErrorMessage": "错误:无效文件类型, 需要带有'py'后缀的Python脚本文件", + "requirementFileUploadErrorMessage": "错误:无效文件类型, 需要requirements.txt 文件" + }, + "Endpoint": "服务端点", + "Exchange": "Exchange", + "Cluster": "Cluster", + "LogMessage": "日志消息", + "KubeFATE":"KubeFATE", + "Unknown": "未知的", + "Creating": "创建中", + "Ready": "准备就绪", + "Dismissed": "解散", + "Unavailable": "不可用的", + "Deleting": "删除中", + "Active": "活动中", + "Installing":"安装中", + "Failed": "失败", + "Removing": "移除中", + "FATEExchange": "FATE Exchange", + "FATECluster": "FATE Cluster", + "OpenFLDirector": "OpenFL Director", + "OpenFLEnvoy": "OpenFL Envoy", + "StepCA":"内置CA服务器(StepCA)", + "PulsarServer":"Pulsar服务器", + "Healthy":"健康", + "Unhealthy":"不健康", + "en": "English", + "zh_CN": "中文 (简体)", + "success": "成功", + "new": "生成新的证书", + "existing": "使用现有的证书", + "InstallingDirector": "安装Director中", + "ConfiguringInfra": "配置基础设施中", + "InstallingEndpoint": "安装服务端点中", + "ConfiguringEndpoint": "配置服务端点中", + "ConfiguringEnvoy": "配置Envoy中", + "InstallingEnvoy": "安装Envoy中" +} diff --git a/frontend/src/assets/lifecycle.png b/frontend/src/assets/lifecycle.png new file mode 100644 index 00000000..9392d161 Binary files /dev/null and b/frontend/src/assets/lifecycle.png differ diff --git a/frontend/src/assets/none.jpg b/frontend/src/assets/none.jpg new file mode 100644 index 00000000..5f07af51 Binary files /dev/null and b/frontend/src/assets/none.jpg differ diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts new file mode 100644 index 00000000..3612073b --- /dev/null +++ b/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts new file mode 100644 index 00000000..f56ff470 --- /dev/null +++ b/frontend/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 00000000..d577e069 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,18 @@ + + + + + Lifecycle Manager + + + + + + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 00000000..c7b673cf --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/frontend/src/polyfills.ts b/frontend/src/polyfills.ts new file mode 100644 index 00000000..373f538a --- /dev/null +++ b/frontend/src/polyfills.ts @@ -0,0 +1,65 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ b/frontend/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/frontend/src/test.ts b/frontend/src/test.ts new file mode 100644 index 00000000..20423564 --- /dev/null +++ b/frontend/src/test.ts @@ -0,0 +1,25 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/frontend/src/utils/auth-interceptor.ts b/frontend/src/utils/auth-interceptor.ts new file mode 100644 index 00000000..ddf19152 --- /dev/null +++ b/frontend/src/utils/auth-interceptor.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Router } from '@angular/router' +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { MessageService } from '../app/components/message/message.service' +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor (public router: Router,private $msg: MessageService) { + } + intercept(req: HttpRequest, next: HttpHandler): Observable> { + let operationSet = new Set(['connect', 'scan', 'check', 'login', 'logout', 'upload']); + let newReq: HttpRequest + // get language package + if (req.url.indexOf('../assets') === -1) { + newReq = req.clone({ + url: '/api/v1' + req.url + }) + } else {// other request + newReq = req.clone() + } + return next.handle(newReq).pipe( + tap( + (res:any) => { + if (res.status === 200 && req.method!== 'GET') { + const str = req.url.split('/') + this.$msg.close() + if (req.method === 'POST') { + var lastWord = str[str.length-1] + if (operationSet.has(lastWord)) { + if (lastWord !== 'login') { + this.$msg.success('serverMessage.'+ str[str.length-1], 2000) + } + } else { + this.$msg.success('serverMessage.create200',2000) + } + } else if (req.method === 'PUT') { + this.$msg.success('serverMessage.update200',2000) + } else if (req.method === 'DELETE') { + this.$msg.success('serverMessage.delete200',2000) + } + } + return res + }, + (err: HttpErrorResponse) => { + if (err.status === 401 && this.router.url.indexOf('/login') === -1) { + sessionStorage.removeItem('lifecycleManager-redirect') + const url = this.router.url + if (newReq.url.indexOf('/user/current')!== -1) { + this.router.navigateByUrl(`/login`) + } else { + if (url !== '/federation' && url !== '/timeout') { + sessionStorage.setItem('lifecycleManager-redirect', url) + } + this.router.navigate(['/login/']) + } + } else if (err.status === 404) { + this.$msg.error('serverMessage.default404') + } + return err + } + ) + ) + } + +} \ No newline at end of file diff --git a/frontend/src/utils/comparator.ts b/frontend/src/utils/comparator.ts new file mode 100644 index 00000000..f0e55747 --- /dev/null +++ b/frontend/src/utils/comparator.ts @@ -0,0 +1,47 @@ +import { ClrDatagridComparatorInterface } from '@clr/angular'; + +export class CustomComparator implements ClrDatagridComparatorInterface { + + fieldName: string; + type: string; + + constructor(fieldName: string, type: string) { + this.fieldName = fieldName; + this.type = type; + } + + compare(a: { [key: string]: any | any[] }, b: { [key: string]: any | any[] }) { + let comp = 0; + if (a && b) { + let fieldA, fieldB; + for (let key of Object.keys(a)) { + if (key === this.fieldName) { + fieldA = a[key]; + fieldB = b[key]; + break; + } else if (typeof a[key] === 'object') { + let insideObject = a[key]; + for (let insideKey in insideObject) { + if (insideKey === this.fieldName) { + fieldA = insideObject[insideKey]; + fieldB = b[key][insideKey]; + break; + } + } + } + } + switch (this.type) { + case "number": + comp = fieldB - fieldA; + break; + case "date": + comp = new Date(fieldB).getTime() - new Date(fieldA).getTime(); + break; + case "string": + comp = fieldB.localeCompare(fieldA); + break; + } + } + return comp; + } +} \ No newline at end of file diff --git a/frontend/src/utils/compile.ts b/frontend/src/utils/compile.ts new file mode 100644 index 00000000..b7d33a83 --- /dev/null +++ b/frontend/src/utils/compile.ts @@ -0,0 +1,17 @@ +export function compile (code: string) { + let newStr = String.fromCharCode(code.charCodeAt(0) + code.length) + for (let i = 1; i < code.length; i++){ + newStr += String.fromCharCode(code.charCodeAt(i) + code.length) + } + return escape(newStr) +} + +export function uncompile (code: string) { + code = unescape(code) + + let newStr = String.fromCharCode(code.charCodeAt(0) - code.length) + for (let i = 1; i < code.length; i++) { + newStr += String.fromCharCode(code.charCodeAt(i) - code.length) + } + return newStr +} \ No newline at end of file diff --git a/frontend/src/utils/constant.ts b/frontend/src/utils/constant.ts new file mode 100644 index 00000000..1d4ef03f --- /dev/null +++ b/frontend/src/utils/constant.ts @@ -0,0 +1,171 @@ +interface ConstantModel { + [key: string]: number +} +interface ResModel { + name: string + value: number +} + +export const INFRATYPE:ConstantModel = { + Unknown: 0 +} + +export const ENDPOINTSTATUS:ConstantModel = { + Unknown: 0, + Creating: 1, + Ready: 2, + Dismissed: 3, + Unavailable: 4, + Deleting: 5 +} +export const CHARTTYPE:ConstantModel = { + Unknown: 0, + FATEExchange: 1, + FATECluster: 2, + OpenFLDirector: 3, + OpenFLEnvoy: 4 +} + +export const ParticipantFATEStatus:ConstantModel = { + Unknown: 0, + Active: 1, + Installing: 2, + Removing: 3, + Reconfiguring: 4, + Failed: 5 +} +export const ParticipantFATEType :ConstantModel = { + Unknown: 0, + Exchange: 1, + Cluster: 2 +} + +export const CerificateServiceType :ConstantModel = { + Unknown: 0, + ATS: 1, + PulsarServer: 2, + FMLManagerServer: 3, + FMLManagerClient: 4, + SitePortalServer: 5, + SitePortalClient: 6, + OpenFLDirector: 101, + OpenFLJupyter: 102, + OpenFLEnvoy: 103 +} + +export const CAType :ConstantModel = { + Unknown: 0, + StepCA: 1, +} +export const BindType: ConstantModel= { + Unknown: 0, + skip:1, + existing: 2, + new: 3 +} + +export const ServiceType: ConstantModel= { + Unknown: 0, + LoadBalancer: 1, + NodePort: 2 +} + +export const EventType: ConstantModel= { + Unknown: 0, + LogMessage: 1 +} + +export const CaStatus: ConstantModel= { + Unknown: 0, + Unhealthy: 1, + Healthy: 2 +} + +export const EnvoyStatus: ConstantModel= { + Unknown: 0, + Active: 1, + Removing: 2, + Failed: 3, + InstallingDirector: 4, + ConfiguringInfra: 5, + InstallingEndpoint: 6, + InstallingEnvoy: 7 +} + +export const Director: ConstantModel= { + Unknown: 0, + Active: 1, + Removing: 2, + Failed: 3, + InstallingDirector: 4, + ConfiguringInfra: 5, + InstallingEndpoint: 6, + InstallingEnvoy: 7 +} + + +export function constantGather (inter: string, state: number) { + let res: ResModel + switch (inter) { + case 'infratype': + res = forObj(INFRATYPE, state) + break; + case 'endpointstatus': + res = forObj(ENDPOINTSTATUS, state) + break; + case 'charttype': + res = forObj(CHARTTYPE, state) + break; + case 'participantFATEstatus': + res = forObj(ParticipantFATEStatus, state) + break; + case 'participantFATEtype': + res = forObj(ParticipantFATEType, state) + break; + case 'cerificateType': + res = forObj(CerificateServiceType, state) + break; + case 'caType': + res = forObj(CAType, state) + break; + case 'bindType': + res = forObj(BindType, state) + break; + case 'eventType': + res = forObj(EventType, state) + break; + case 'caStatus': + res = forObj(CaStatus, state) + break; + case 'envoy': + res = forObj(EnvoyStatus, state) + break + case 'director': + res = forObj(Director, state) + break + default: + res = { + name: '', + value: 0 + } + break; + } + return res +} + +function forObj (obj: any, state: number): ResModel { + let res: ResModel = { + name: '', + value: 0 + } + for (const key in obj) { + if (obj[key] === state) { + res = { + name: key, + value: state + } + break + } + } + return res +} \ No newline at end of file diff --git a/frontend/src/utils/high-json.ts b/frontend/src/utils/high-json.ts new file mode 100644 index 00000000..3968283e --- /dev/null +++ b/frontend/src/utils/high-json.ts @@ -0,0 +1,93 @@ +export function isCollapsable(arg:any):boolean { + return arg instanceof Object && Object.keys(arg).length > 0; +} +export function isUrl(string:string):boolean { + var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i; + return urlRegexp.test(string); +} +export function json2html(json:string | any, options:any):string { + var html = ''; + if (typeof json === 'string') { + // Escape tags and quotes + json = json + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"'); + + if (options.withLinks && isUrl(json)) { + html += '' + json + ''; + } else { + // Escape double quotes in the rendered non-URL string. + json = json.replace(/"/g, '\\"'); + html += '"' + json + '"'; + } + } else if (typeof json === 'number') { + html += '' + json + ''; + } else if (typeof json === 'boolean') { + html += '' + json + ''; + } else if (json === null) { + html += 'null'; + } else if (json instanceof Array) { + if (json.length > 0) { + html += '[
    '; + for (var i = 0; i < json.length; ++i) { + html += '
  1. '; + // Add toggle button if item is collapsable + if (isCollapsable(json[i])) { + html += ''; + } + html += json2html(json[i], options); + // Add comma if item is not last + if (i < json.length - 1) { + html += ','; + } + html += '
  2. '; + } + html += '
]'; + } else { + html += '[]'; + } + } else if (typeof json === 'object') { + var keyCount = Object.keys(json).length; + if (keyCount > 0) { + html += '{
    '; + for (var key in json) { + if (Object.prototype.hasOwnProperty.call(json, key)) { + html += '
  • '; + var keyRepr = options.withQuotes ? + '"' + key + '"' : key; + // Add toggle button if item is collapsable + if (isCollapsable(json[key])) { + html += '' + keyRepr + ''; + } else { + html += keyRepr; + } + html += ': ' + json2html(json[key], options); + // Add comma if item is not last + if (--keyCount > 0) { + html += ','; + } + html += '
  • '; + } + } + html += '
}'; + } else { + html += '{}'; + } + } + return html; +} +export function valueSplice (arr: string[], obj:any) { + for (const key in obj) { + if (key === arr[0]) { + obj[key] = arr[1] + break + } + if (Object.prototype.toString.call(obj[key]).slice(8) === 'Object]') { + const newObj = obj[key] + valueSplice(arr, newObj) + } + } +} diff --git a/frontend/src/utils/selective-preloading-strategy.ts b/frontend/src/utils/selective-preloading-strategy.ts new file mode 100644 index 00000000..e2b6ecb7 --- /dev/null +++ b/frontend/src/utils/selective-preloading-strategy.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core' +import { PreloadingStrategy, Route } from '@angular/router' +import { Observable } from 'rxjs'; +import { of } from 'rxjs' +@Injectable() +export class SelectivePreloadingStrategy implements PreloadingStrategy { + preload(route: Route, fn: () => Observable): Observable { + if (route.data && route.data.preload) { + return fn() + } else { + return of(null) + } + } + +} \ No newline at end of file diff --git a/frontend/src/utils/validators.ts b/frontend/src/utils/validators.ts new file mode 100644 index 00000000..c2e14142 --- /dev/null +++ b/frontend/src/utils/validators.ts @@ -0,0 +1,261 @@ +import { Validators, AbstractControl, NG_VALIDATORS, FormGroup } from '@angular/forms'; + +function myIndexOf(str: string, condition: string): number { + const newStr = str.toLocaleLowerCase() + const newCondition = condition.toLocaleLowerCase() + return newStr.indexOf(newCondition) +} + +function maxOrMin(value: any, max: number, min: number, defaultValue = null as any, el: groupModel) { + let arr = [defaultValue, [EmptyValidator, value]] + if (el.type[0] === 'notRequired') { + if (value !== null && value !== 'notRequired') { + arr = [defaultValue, value] + } else { + arr = [defaultValue] + } + if (max && min) { + arr = [defaultValue, [value, Validators.maxLength(max), Validators.minLength(min)]] + } else if (max) { + arr = [defaultValue, [value, Validators.maxLength(max)]] + } else if (min) { + arr = [defaultValue, [value, Validators.minLength(min)]] + } + } else { + arr = [defaultValue, [EmptyValidator]] + if (max && min) { + arr = [defaultValue, [EmptyValidator, value, Validators.maxLength(max), Validators.minLength(min)]] + } else if (max) { + arr = [defaultValue, [EmptyValidator, value, Validators.maxLength(max)]] + } else if (min) { + arr = [defaultValue, [EmptyValidator, value, Validators.minLength(min)]] + } else { + if (value) { + arr = [defaultValue, [EmptyValidator, value]] + } + } + } + return arr +} +interface groupModel { + name: string + type: string[], + value?: any + max?: number + min?: number +} +export function ValidatorGroup(group: groupModel[]) { + const newGroup: any = {} + group.forEach(el => { + el.type.forEach(key => { + switch (key) { + case 'number': + newGroup[el.name] = maxOrMin(NumberValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'word': + newGroup[el.name] = maxOrMin(WordValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'noSpace': + newGroup[el.name] = maxOrMin(noSpaceAlphanumericValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'email': + newGroup[el.name] = maxOrMin(EmailValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'internet': + newGroup[el.name] = maxOrMin(InternetValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'ip': + newGroup[el.name] = maxOrMin(IpValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'zero': + + newGroup[el.name] = maxOrMin(zeroToHundred, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'require': + newGroup[el.name] = maxOrMin('', el.max || 0, el.min || 0, el.value || null, el) + break; + case 'json': + newGroup[el.name] = maxOrMin(JsonValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'fqdn': + newGroup[el.name] = maxOrMin(fqdnValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'number-list': + newGroup[el.name] = maxOrMin(NumberListValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'notRequired': + newGroup[el.name] = maxOrMin('notRequired', el.max || 0, el.min || 0, el.value || null, el) + break; + default: + if (el.value) { + newGroup[el.name] = [el.value] + } else { + newGroup[el.name] = [null] + } + break; + } + }) + }) + + return newGroup +} +// +export function EmptyValidator(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (typeof v === 'string' && !v?.trim()) { + return { emptyMessage: 'validator.empty' } + } else if (!v) { + return { emptyMessage: 'validator.empty' } + } + return null +} + +// email +export function EmailValidator(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (myIndexOf(v, '@') === -1) { + return { message: 'validator.email' } + } + return null +} +// numbers +export function NumberValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[0-9]*$/ + const v = control.value + if (v === '' || v===null) { + return null + } + if (!reg.test(v)) { + return { message: 'validator.number' } + } + return null +} +// Chinese, English, numbers +export function WordValidator(control: AbstractControl): { [key: string]: any } | null { + // \u4E00-\u9FA5 + const reg = /^[A-Za-z0-9\s\d\-_/]+$/ + const v = control.value + + if (!reg.test(v?.trim())) { + return { message: 'validator.word' } + } + return null +} +// Chinese, English, numbers +export function noSpaceAlphanumericValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[\u4E00-\u9FA5A-Za-z0-9\d\-_/]+$/ + const v = control.value + + if (!reg.test(v?.trim())) { + return { message: 'validator.noSpace' } + } + return null +} +// English, numbers +export function EgNuValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[\u4E00-\u9FA5A-Za-z0-9]+$/ + const v = control.value + if (!reg.test(v?.trim())) { + return { message: 'validator.engu' } + } + return null +} +// internetUrl +export function InternetValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^(http|https):\/\/([\w]+)\S*/ + const v = control.value + if (v === '' || v === null) { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.internet' } + } + return null +} + +// ip +export function IpValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g + const v = control.value + if (v === '' || v===null) { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.ip' } + } + return null +} + +// 0-100 +export function zeroToHundred (control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (v === ''|| v===null) { + return null + } + + if (+v < 0 || +v >100) { + return { message: 'validator.zeroToHundred' } + } + return null +} + + +// Json +export function JsonValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^\s*\{\s*[A-Z0-9._]+\s*:\s*[A-Z0-9._]+\s*(,\s*[A-Z0-9._]+\s*:\s*[A-Z0-9._]+\s*)*\}\s*$/i + const v = control.value + if (v === '' || v===null || v === '{}') { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.json' } + } + return null +} + +// numbers list +export function NumberListValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[0-9,]*$/ + const v = control.value + if (v === '' || v===null) { + return null + } + if (!reg.test(v)) { + return { message: 'validator.number' } + } + return null +} + + +// FQDN +export function fqdnValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /(?=^.{4,253}$)(^((?!-)[a-z0-9-]{0,62}[a-z0-9]\.)+[a-z]{2,63}$)/gm + const v = control.value + if (v === '' || v===null) { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.fqdn' } + } + return null +} + + +//change password matching +export function ConfirmedValidator(controlName: string, matchingControlName: string){ + + return (formGroup: FormGroup) => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + if (matchingControl.errors && !matchingControl.errors.confirmedValidator) { + return; + } + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ confirmedValidator: true }); + } else { + matchingControl.setErrors(null); + } + + } + +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 00000000..82d91dc4 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..6df82832 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,30 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 00000000..092345b0 --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..d3702b89 --- /dev/null +++ b/go.mod @@ -0,0 +1,222 @@ +module github.com/FederatedAI/FedLCM + +go 1.17 + +require ( + github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b + github.com/Masterminds/sprig/v3 v3.2.2 + github.com/appleboy/gin-jwt/v2 v2.8.0 + github.com/gin-contrib/logger v0.2.2 + github.com/gin-gonic/gin v1.8.1 + github.com/hashicorp/go-version v1.6.0 + github.com/json-iterator/go v1.1.12 + github.com/mitchellh/mapstructure v1.5.0 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.27.0 + github.com/satori/go.uuid v1.2.0 + github.com/sirupsen/logrus v1.8.1 + github.com/smallstep/certificates v0.20.0 + github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.0 + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe + github.com/swaggo/gin-swagger v1.5.0 + github.com/swaggo/swag v1.8.3 + github.com/urfave/cli/v2 v2.10.3 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.3.7 + gorm.io/gorm v1.23.6 + k8s.io/api v0.24.2 + k8s.io/apimachinery v0.24.2 + k8s.io/client-go v0.24.2 + k8s.io/kubectl v0.24.2 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + filippo.io/edwards25519 v1.0.0 // indirect + github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/squirrel v1.5.3 // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect + github.com/chzyer/readline v1.5.0 // indirect + github.com/containerd/containerd v1.6.6 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/badger v1.6.2 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v20.10.17+incompatible // indirect + github.com/docker/docker-credential-helpers v0.6.4 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-chi/chi v4.1.2+incompatible // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.0.2 // indirect + github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.0 // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.9.8 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.15.7 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/lib/pq v1.10.6 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/micromdm/scep/v2 v2.1.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/naoina/go-stringutil v0.1.0 // indirect + github.com/naoina/toml v0.1.1 // indirect + github.com/newrelic/go-agent v3.17.0+incompatible // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.35.0 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/rubenv/sql-migrate v1.1.2 // indirect + github.com/russross/blackfriday v1.6.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/slackhq/nebula v1.5.2 // indirect + github.com/smallstep/nosql v0.4.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.0 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + github.com/urfave/cli v1.22.9 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect + go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect + go.step.sm/cli-utils v0.7.3 // indirect + go.step.sm/crypto v0.16.2 // indirect + go.step.sm/linkedca v0.16.1 // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect + golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/tools v0.1.11 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 // indirect + google.golang.org/grpc v1.47.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.3.4 // indirect + gorm.io/driver/sqlite v1.3.5 // indirect + helm.sh/helm/v3 v3.9.0 // indirect + k8s.io/apiextensions-apiserver v0.24.2 // indirect + k8s.io/apiserver v0.24.2 // indirect + k8s.io/cli-runtime v0.24.2 // indirect + k8s.io/component-base v0.24.2 // indirect + k8s.io/klog/v2 v2.70.0 // indirect + k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect + oras.land/oras-go v1.2.0 // indirect + sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect + sigs.k8s.io/kustomize/api v0.11.5 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.7 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..c2816f4c --- /dev/null +++ b/go.sum @@ -0,0 +1,1946 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v0.1.0 h1:W2vbGCrE3Z7J/x3WXLxxGl9LMSB2uhsAA7Ss/6u/qRY= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/security v1.3.0/go.mod h1:pQsnLAXfMzuWVJdctBs8BV3tGd3Jr0SMYu6KK3QXYAs= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-sdk-for-go v58.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b h1:2RoqwJw4QTJ/heCASun5F5m/kf6y86jIod/pgZ2MWYA= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b/go.mod h1:3/tZ0DwSUKl06W2exOUDLk5D9plmtwjIoIhFtVZwpTY= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= +github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/appleboy/gin-jwt/v2 v2.8.0 h1:Glo7cb9eBR+hj8Y7WzgfkOlqCaNLjP+RV4dNO3fpdps= +github.com/appleboy/gin-jwt/v2 v2.8.0/go.mod h1:KsK7E8HTvRg3vOiumTsr/ntNTHbZ3IbHLe4Eto31p7k= +github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= +github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= +github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0= +github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk= +github.com/gin-contrib/logger v0.2.2 h1:xIoUvRdmfID02X09wfq7wuWmevBTdMK1T6TQjbv5r+4= +github.com/gin-contrib/logger v0.2.2/go.mod h1:6uKBteCGZF6VtxSfO1MKWl7aEu1sPSOhwCEAFPFxnnI= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE= +github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= +github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= +github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= +github.com/hashicorp/vault/api/auth/kubernetes v0.1.0/go.mod h1:Pdgk78uIs0mgDOLvc3a+h/vYIT9rznw2sz+ucuH9024= +github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= +github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= +github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f/go.mod h1:nwPd6pDNId/Xi16qtKrFHrauSwMNuvk+zcjk89wrnlA= +github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/newrelic/go-agent v3.17.0+incompatible h1:LyTITE6HoZ/5DyB9R0YTJB6QKw5JArKakfkX/Gg79P8= +github.com/newrelic/go-agent v3.17.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.35.0 h1:Eyr+Pw2VymWejHqCugNaQXkAi6KayVNxaHeu6khmFBE= +github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= +github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/slackhq/nebula v1.5.2 h1:wuIOHsOnrNw3rQx8yPxXiGu8wAtAxxtUI/K8W7Vj7EI= +github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIpR52WlE= +github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= +github.com/smallstep/certificates v0.20.0 h1:i9nxswqi4W+4XzIiM2LYhOhaN6LXNGXhRCwXtg6jUvQ= +github.com/smallstep/certificates v0.20.0/go.mod h1:QlIuU3l25qxeWTo19VviZq/CKhRFtJt75RoTEUh9pbY= +github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M= +github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/gin-swagger v1.5.0 h1:hlLbxPj6qvbtX2wpbsZuOIlcnPRCUDGccA0zMKVNpME= +github.com/swaggo/gin-swagger v1.5.0/go.mod h1:3mKpZClKx7mnUGsiwJeEkNhnr1VHMkMaTAXIoFYUXrA= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s= +github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= +github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v2 v2.10.3 h1:oi571Fxz5aHugfBAJd5nkwSk3fzATXtMlpxdLylSCMo= +github.com/urfave/cli/v2 v2.10.3/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= +go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= +go.step.sm/cli-utils v0.7.3 h1:IA12IaiXVCI18yOFVQuvMpyvjL8wuwUn1yO+KhAVAr0= +go.step.sm/cli-utils v0.7.3/go.mod h1:RJRwbBLqzs5nrepQLAV9FuT3fVpWz66tKzLIB7Izpfk= +go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= +go.step.sm/crypto v0.16.2 h1:Pr9aazTwWBBZNogUsOqhOrPSdwAa9pPs+lMB602lnDA= +go.step.sm/crypto v0.16.2/go.mod h1:1WkTOTY+fOX/RY4TnZREp6trQAsBHRQ7nu6QJBiNQF8= +go.step.sm/linkedca v0.16.1 h1:CdbMV5SjnlRsgeYTXaaZmQCkYIgJq8BOzpewri57M2k= +go.step.sm/linkedca v0.16.1/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 h1:uBgVQYJLi/m8M0wzp+aGwBWt90gMRoOVf+aWTW10QHI= +golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b h1:NXqSWXSRUSCaFuvitrWtU169I3876zRTalMRbfd6LL0= +golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.1/go.mod h1:EApyTk/ZNrkbZjurHL1nleDYnsPpJYBO7LZEBCyDAHk= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 h1:W70HjnmXFJm+8RNjOpIDYW2nKsSi/af0VvIZUtYkwuU= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q= +gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= +gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= +gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/driver/sqlite v1.3.5 h1:VmtQcbtN13YCUy8QNpKBBYklH0LMO7yQcmFGvRIJ/ws= +gorm.io/driver/sqlite v1.3.5/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= +gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +helm.sh/helm/v3 v3.9.0 h1:qDSWViuF6SzZX5s5AB/NVRGWmdao7T5j4S4ebIkMGag= +helm.sh/helm/v3 v3.9.0/go.mod h1:fzZfyslcPAWwSdkXrXlpKexFeE2Dei8N27FFQWt+PN0= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI= +k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= +k8s.io/apiextensions-apiserver v0.24.2 h1:/4NEQHKlEz1MlaK/wHT5KMKC9UKYz6NZz6JE6ov4G6k= +k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ= +k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM= +k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apiserver v0.24.2 h1:orxipm5elPJSkkFNlwH9ClqaKEDJJA3yR2cAAlCnyj4= +k8s.io/apiserver v0.24.2/go.mod h1:pSuKzr3zV+L+MWqsEo0kHHYwCo77AT5qXbFXP2jbvFI= +k8s.io/cli-runtime v0.24.2 h1:KxY6tSgPGsahA6c1/dmR3uF5jOxXPx2QQY6C5ZrLmtE= +k8s.io/cli-runtime v0.24.2/go.mod h1:1LIhKL2RblkhfG4v5lZEt7FtgFG5mVb8wqv5lE9m5qY= +k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA= +k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= +k8s.io/code-generator v0.24.2/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= +k8s.io/component-base v0.24.2 h1:kwpQdoSfbcH+8MPN4tALtajLDfSfYxBDYlXobNWI6OU= +k8s.io/component-base v0.24.2/go.mod h1:ucHwW76dajvQ9B7+zecZAP3BVqvrHoOxm8olHEg0nmM= +k8s.io/component-helpers v0.24.2/go.mod h1:TRQPBQKfmqkmV6c0HAmUs8cXVNYYYLsXy4zu8eODi9g= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.70.0 h1:GMmmjoFOrNepPN0ZeGCzvD2Gh5IKRwdFx8W5PBxVTQU= +k8s.io/klog/v2 v2.70.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= +k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 h1:yEQKdMCjzAOvGeiTwG4hO/hNVNtDOuUFvMUZ0OlaIzs= +k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8/go.mod h1:mbJ+NSUoAhuR14N0S63bPkh8MGVSo3VYSGZtH/mfMe0= +k8s.io/kubectl v0.24.2 h1:+RfQVhth8akUmIc2Ge8krMl/pt66V7210ka3RE/p0J4= +k8s.io/kubectl v0.24.2/go.mod h1:+HIFJc0bA6Tzu5O/YcuUt45APAxnNL8LeMuXwoiGsPg= +k8s.io/metrics v0.24.2/go.mod h1:5NWURxZ6Lz5gj8TFU83+vdWIVASx7W8lwPpHYCqopMo= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= +oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 h1:2sgAQQcY0dEW2SsQwTXhQV4vO6+rSslYx8K3XmM5hqQ= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI= +sigs.k8s.io/kustomize/api v0.11.5 h1:vLDp++YAX7iy2y2CVPJNy9pk9CY8XaUKgHkjbVtnWag= +sigs.k8s.io/kustomize/api v0.11.5/go.mod h1:2UDpxS6AonWXow2ZbySd4AjUxmdXLeTlvGBC46uSiq8= +sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco= +sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg= +sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg= +sigs.k8s.io/kustomize/kyaml v0.13.7 h1:/EZ/nPaLUzeJKF/BuJ4QCuMVJWiEVoI8iftOHY3g3tk= +sigs.k8s.io/kustomize/kyaml v0.13.7/go.mod h1:6K+IUOuir3Y7nucPRAjw9yth04KSWBnP5pqUTGwj/qU= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/helm-charts/.gitignore b/helm-charts/.gitignore new file mode 100644 index 00000000..fa9933c6 --- /dev/null +++ b/helm-charts/.gitignore @@ -0,0 +1,14 @@ +*.cfg +*.idea/ +*.vscode/ +*output/ +*.todo +*.exe +dist/ +*.egg-info +*.test +.DS_Store +*.out +*.tgz +*.tar +partyId-values.yaml diff --git a/helm-charts/README.md b/helm-charts/README.md new file mode 100644 index 00000000..bd5b1e4f --- /dev/null +++ b/helm-charts/README.md @@ -0,0 +1,121 @@ +Charts for FATE deployment by the lifecycle-manager. The lifecycle-manager service uses it internally. It is unnecessary to use these charts directly unless advanced changes are required. +### Chart package + +```bash +$ helm package ./fate +$ helm package ./fate-exchange +``` + +Using the command will generate a chart package of `*.tgz`. + +### Chart verify + +```bash +$ helm lint ./fate +$ helm lint ./fate-exchange +``` + +After modifying chart and before submitting the code, run verification to check whether there are obvious errors. + +# Installation Guide + +Use the 'kubefate' command line to upload chart and then deploy site-portal. For kubefate installation and use, refer to [this](https://github.com/FederatedAI/KubeFATE/tree/v1.6.1/k8s-deploy). + +#### Clone + +```bash +$ git clone +``` + +#### Package Charts + +```bash +$ cd /helm-charts/charts +$ helm package ./fate +$ helm package ./fate-exchange +``` + +#### Upload Chart + +```bash +$ kubefate chart upload -f fate-${version}.tgz +``` + +```bash +$ kubefate chart upload -f fate-exchange-${version}.tgz +``` + +#### Edit cluster configuration + +Modify the cluster configuration `site-portal.yaml` and `fml-manager.yaml` + +```bash +$ vi site-portal.yaml +``` + +```bash +$ vi fml-manager.yaml +``` + +#### Generate secret key + +The secret of both parties needs to be generated before deployment, refer [here](https://github.com/FederatedAI/KubeFATE/blob/v1.6.1/docs/FATE_On_Spark_With_Pulsar.md#certificate-generation). In the steps of generating the secret key, you can get the secret key file of the corresponding party, including `certs/ca.cert.pem`, for the party it is `${party_id}.fate.org/broker.cert.pem` and `${party_id }.fate.org/broker.key-pk8.pem`, the corresponding exchange is `proxy.fate.org/proxy.cert.pem` and `proxy.fate.org/proxy.key.pem`. + +```bash +# for party, import the certificate to k8s +$ kubectl create secret generic pulsar-cert -n {namespace} \ + --from-file=broker.cert.pem=9999.fate.org/broker.cert.pem \ + --from-file=broker.key-pk8.pem=9999.fate.org/broker.key-pk8.pem \ + --from-file=ca.cert.pem=certs/ca.cert.pem + +``` + +```bash +# for exchange, import the certificate to k8s +$ kubectl create secret generic traffic-server-cert -n {namespace} \ + --from-file=proxy.cert.pem=proxy.fate.org/proxy.cert.pem \ + --from-file=proxy.key.pem=proxy.fate.org/proxy.key.pem \ + --from-file=ca.cert.pem=certs/ca.cert.pem +``` + +#### Deploy + +```bash +# party +$ kubefate cluster install -f site-portal.yaml +``` +```bash +# exchange +$ kubefate cluster install -f fml-manager.yaml +``` + +#### Check status + +When you deploy the cluster, you will get a `job_UUID` + +```bash +# View deployment status +$ kubefate job describe ${job_UUID} +``` + +When the job status is `Success`, it indicates that the deployment is successful, and then configure the host file as `${URL}` according to `ingress.frontend.hosts[0].name` in `site-portal.yaml`. + +It can also be obtained by command `kubectl get ingress -n ${FATE_namespace}`. + +```bash +$ echo "${Ingress_IP} ${URL}" >> /etc/hosts +``` + +> `${Ingress_IP}` is the IP to deploy ingress controller. It may be Node IP or Load Balancer IP depending on the deployment method. + +Then open `${URL}` through the browser to use the site-portal. + +When using site-portal, you need the IP and port of FATE-flow, which can be retrieved via `kubectl` command + +```bash +$ kubectl get pod -n ${FATE_namespace} -l fateMoudle=python -o wide +``` + +This command can get the cluster IP of fate flow Pod. Port is the default 9380. + +Follow the site-portal document to configure the service and start using it for FML management. \ No newline at end of file diff --git a/helm-charts/README_OpenFL_Director.md b/helm-charts/README_OpenFL_Director.md new file mode 100644 index 00000000..1110d6bc --- /dev/null +++ b/helm-charts/README_OpenFL_Director.md @@ -0,0 +1,192 @@ +Charts for OpenFL Director deployment. The lifecycle-manager service uses it internally. Please contact the maintainers of this project for its detailed usage. + +### Chart package + +```bash +helm package ./openfl-director +``` + +Using the command will generate a chart package of `*.tgz`. + +### Chart verify + +```bash +$ helm lint ./openfl-director +``` + +After modifying chart and before submitting the code, run verification to check whether there are obvious errors. + +# Installation Guide + +Use the 'kubefate' command line to upload chart and then deploy the application. For kubefate installation and use, refer to [this](https://github.com/FederatedAI/KubeFATE/tree/v1.6.1/k8s-deploy). + +### Clone + +```bash +$ git clone +``` + +### Package Charts + +```bash +$ cd cd /helm-charts/charts +$ helm package ./openfl-director +``` + +### Upload Chart + +```bash +$ kubefate chart upload -f openfl-director-${version}.tgz +``` +*** +### Generate certificates +This example uses example steps from KubeFATE project for pulsar deployment. Although the application is different, the steps are similar. +#### Generate the secret key +Preparations: +```bash +$ mkdir my-ca +$ cd my-ca +$ wget https://raw.githubusercontent.com/apache/pulsar/master/site2/website/static/examples/openssl.cnf +$ export CA_HOME=$(pwd) +$ mkdir certs crl newcerts private +$ chmod 700 private/ +``` +Generate the private key for root certificate: +```bash +$ openssl genrsa -aes256 -out private/ca.key.pem 4096 +``` +Need to add a password for the private key, then: +```bash +$ chmod 400 private/ca.key.pem +``` +The database files for the certificate management: +```bash +$ touch index.txt +$ echo 1000 > serial +``` +Generate the root certificate: +```bash +$ openssl req -config openssl.cnf -key private/ca.key.pem \ + -new -x509 -days 7300 -sha256 -extensions v3_ca \ + -out certs/ca.cert.pem +``` +No need to fill anything about the prompt questions, then: +```bash +$ chmod 444 certs/ca.cert.pem +``` +Once the above commands are completed, the CA-related certificates and keys have been generated, they are: + +* certs/ca.cert.pem: the certification of CA +* private/ca.key.pem: the key of CA + +The next step is to generate 2 pairs of private keys and certificates for both Director and Notebook: +```bash +$ mkdir director +$ openssl genrsa -out director/priv.key 2048 +$ openssl req -config openssl.cnf -key director/priv.key -new -sha256 -out director/director.csr +``` +When it prompts "common name", make sure you fill in "director", Then generate the certification. +```bash +openssl ca -config openssl.cnf -extensions server_cert -days 10000 -notext -md sha256 -in director/director.csr -out director/director.crt +``` +```bash +$ mkdir notebook +$ openssl genrsa -out notebook/priv.key 2048 +$ openssl req -config openssl.cnf -key notebook/priv.key -new -sha256 -out notebook/notebook.csr +``` +No need to fill anything for the prompts. +```bash +$ openssl ca -config openssl.cnf -days 10000 -notext -md sha256 -in notebook/notebook.csr -out notebook/notebook.crt +``` + +After doing all the generation, run 2 kubectl commands to add above generated files to kubernetes cluster as a secret resource: +```bash +$ kubectl -n openfl-director create secret generic director-cert --from-file=director.crt=director/director.crt --from-file=priv.key=director/priv.key --from-file=root_ca.crt=certs/ca.cert.pem +$ kubectl -n openfl-director create secret generic notebook-cert --from-file=notebook.crt=notebook/notebook.crt --from-file=priv.key=notebook/priv.key --from-file=root_ca.crt=certs/ca.cert.pem +``` +*** +### Deploy +### Edit cluster configuration + +We can create a file called `od_cluster.yaml` and add below contents. This is example is trying to create a director for the [Tensorflow_MNIST](https://github.com/intel/openfl/tree/develop/openfl-tutorials/interactive_api/Tensorflow_MNIST) tutorial: +``` +name: openfl-director +namespace: openfl-director +chartName: openfl-director +chartVersion: v0.1.0 +registry: "federatedai" +pullPolicy: IfNotPresent +modules: + - director + - notebook + +director: + image: fedlcm-openfl + imageTag: v0.1.0 + sampleShape: "['784']" + targetShape: "['1']" + envoyHealthCheckPeriod: 60 + type: NodePort + +notebook: + image: fedlcm-openfl + imageTag: v0.1.0 + type: NodePort + +ingress: + notebook: + hosts: + - name: notebook.openfl.example.com +``` + +### (Optional) Configure a password for login the Jupyter notebook +We can configure the above `notebook` part as below: +``` +notebook: + image: fedlcm-openfl + imageTag: v0.1.0 + password: argon2:$argon2id$v=19$m=10240,t=10,p=8$TmW50aM7Fey2lNrU7kpOhQ$s4SY7l8QItxgR9iwVA+DTc2uwGnawh1p1dB42bbLH48 + type: NodePort + nodePort: 30888 +``` +The password is a hashed password, in this example it's original text is `admin`. +The hashed string is generated by python console, such as: +```python +>>> from notebook.auth import passwd +>>> passwd("admin") +'argon2:$argon2id$v=19$m=10240,t=10,p=8$TmW50aM7Fey2lNrU7kpOhQ$s4SY7l8QItxgR9iwVA+DTc2uwGnawh1p1dB42bbLH48' +``` +Note before that you need to run `pip install juputer` +The on the UI, when the password is required, type in `admin` will work. +More information is [here](https://jupyter-notebook.readthedocs.io/en/stable/public_server.html#adding-hashed-password-to-your-notebook-configuration-file). + + +### Deploy and check status + +Now we can install above cluster: +```bash +$ kubefate cluster install -f od-cluster.yaml +``` + +When you deploy the cluster, you will get a `job_UUID` + +```bash +# View deployment status +$ kubefate job describe ${job_UUID} +``` + +When the job status is` Success`, it indicates that the deployment is successful. +configure the host file as `${URL}`according to `ingress.notebook.hosts[0].name`in`od-cluster.yaml`. + +Then we can access the Jupyter notebook by typing `http://notebook.openfl.example.com/lab`. + +Alternatively, if the service type of the notebook service is NodePort or LoadBalancer, this notebook can also be accessed by typing `http//::` + +In the notebook, verify that the notebook can connect to the director successfully, which means that below code can execute: +```python +from openfl.interface.interactive_api.federation import Federation + +federation = Federation() +``` + +For the `Tensorflow_MNIST` example, open the notebook in the `interactive_api/Tensorflow_MNIST/workspace` folder and change the second cell to above code. Once the envoys have been deployed, you can follow the notebook to run the training "experiment". diff --git a/helm-charts/README_OpenFL_Envoy.md b/helm-charts/README_OpenFL_Envoy.md new file mode 100644 index 00000000..7dd44190 --- /dev/null +++ b/helm-charts/README_OpenFL_Envoy.md @@ -0,0 +1,171 @@ +Charts for OpenFL Envoy deployment. The lifecycle-manager service uses it internally. Please contact the maintainers of this project for its detailed usage. + +### Chart package + +```bash +helm package ./openfl-envoy +``` + +Using the command will generate a chart package of `*.tgz`. + +### Chart verify + +```bash +$ helm lint ./openfl-envoy +``` + +After modifying chart and before submitting the code, run verification to check whether there are obvious errors. + +# Installation Guide + +Use the 'kubefate' command line to upload chart and then deploy the application. For kubefate installation and use, refer to [this](https://github.com/FederatedAI/KubeFATE/tree/v1.6.1/k8s-deploy). + +### Clone + +```bash +$ git clone +``` + +### Package Charts + +```bash +$ cd cd /helm-charts/charts +$ helm package ./openfl-envoy +``` + +### Upload Chart + +```bash +$ kubefate chart upload -f openfl-envoy-${version}.tgz +``` + +### Generate certificates +This example uses example steps from KubeFATE project for pulsar deployment. Although the application is different, the steps are similar. + +**If you have already created the root CA certificate, during deploying the Director, you can jump directly to create Envoy certificate** +#### Generate the secret key +Preparations: +```bash +$ mkdir my-ca +$ cd my-ca +$ wget https://raw.githubusercontent.com/apache/pulsar/master/site2/website/static/examples/openssl.cnf +$ export CA_HOME=$(pwd) +$ mkdir certs crl newcerts private +$ chmod 700 private/ +``` +Generate the private key for root certificate: +```bash +$ openssl genrsa -aes256 -out private/ca.key.pem 4096 +``` +Need to add a password for the private key, then: +```bash +$ chmod 400 private/ca.key.pem +``` +The database files for the certificate management: +```bash +$ touch index.txt +$ echo 1000 > serial +``` +Generate the root certificate: +```bash +$ openssl req -config openssl.cnf -key private/ca.key.pem \ + -new -x509 -days 7300 -sha256 -extensions v3_ca \ + -out certs/ca.cert.pem +``` +No need to fill anything about the prompt questions, then: +```bash +$ chmod 444 certs/ca.cert.pem +``` +Once the above commands are completed, the CA-related certificates and keys have been generated, they are: + +* certs/ca.cert.pem: the certification of CA +* private/ca.key.pem: the key of CA + +The next step is to generate another of private keys and certificates for Envoy: +```bash +$ mkdir envoy-1 +$ openssl genrsa -out envoy-1/priv.key 2048 +$ openssl req -config openssl.cnf -key envoy-1/priv.key -new -sha256 -out envoy-1/envoy.csr +``` +When it prompts "common name", it should be a unique name, such as "envoy-1". +```bash +openssl ca -config openssl.cnf -days 10000 -notext -md sha256 -in envoy-1/envoy.csr -out envoy-1/envoy.crt +``` + +After doing all the generating, run below command to add above generated files to kubernetes cluster as a secret: +```bash +$ kubectl create namespace openfl-envoy-1 +$ kubectl -n openfl-envoy-1 create secret generic envoy-cert --from-file=envoy.crt=envoy-1/envoy.crt --from-file=priv.key=envoy-1/priv.key --from-file=root_ca.crt=certs/ca.cert.pem +``` + +### Prepare the envoy shard descriptor file +Every Envoy is matched to one shard descriptor in order to run. When the Director starts an experiment, the Envoy accepts the experiment workspace, prepares the environment, and starts a Collaborator. +The shard descriptor is a python file. In this document, we use one example from [here](https://github.com/intel/openfl/blob/develop/openfl-tutorials/interactive_api/Tensorflow_MNIST/envoy/mnist_shard_descriptor.py), as the example in the Director document is also using this one. +For more information, please check [here](https://openfl.readthedocs.io/en/latest/running_the_federation.html?highlight=shard%20descriptor%20#step-2-start-the-envoy), and also search for `shard_descriptor` +on this [page](https://openfl.readthedocs.io/en/latest/running_the_federation.html?highlight=shard%20descriptor%20#run-the-federation). + +The user is responsible for preparing a directory called `python` and put the shard descriptor python file as well as an optional `requirements.txt` file containing necessary python libs the python code needs into it, and add it as a configmap: +```bash +$ kubectl create configmap envoy-python-configs --from-file=python/ -n openfl-envoy-1 +``` + +**NOTE**: due to the limitation of configmap rules, all filenames can only contain alphanumeric characters, `-`, `_` or `.`, other characters, including `space`, are not allowed. + +### Edit cluster configuration + +We can create a file called `oe_cluster.yaml` and add below contents. For the [Tensorflow_MNIST](https://github.com/intel/openfl/tree/develop/openfl-tutorials/interactive_api/Tensorflow_MNIST) example, the content would be: +``` +name: envoy-1 +namespace: openfl-envoy-1 +chartName: openfl-envoy +chartVersion: v0.1.0 +registry: "" +pullPolicy: IfNotPresent +podSecurityPolicy: + enabled: false +modules: + - envoy + +envoy: + image: fedlcm-openfl + imageTag: v0.1.0 + directorFqdn: director + directorIp: 192.168.1.1 + directorPort: 50051 + aggPort: 50052 + envoyConfigs: + params: + cuda_devices: [] + optional_plugin_components: {} + shard_descriptor: + template: mnist_shard_descriptor.MnistShardDescriptor + params: + rank_worldsize: 1, 2 +``` + +There are several things to note: +* The `name` field must be the `common name` configured in the certificate generated above. +* The `directorFqdn` field must be the `common name` configured in the director's certificate. +* The `directorIp` is the director service's exposed IP address. This can vary based on the director service's type. +* The `directorPort` and `aggPort` are also the exposed ports in the director's service. In general, these settings are the access information of the director service. +* The content for `envoyConfigs` is the yaml config that will be feed to envoy. + +### Deploy and check status + +Now we can install above cluster: +```bash +$ kubefate cluster install -f oe-cluster.yaml +``` + +When you deploy the cluster, you will get a `job_UUID` + +```bash +# View deployment status +$ kubefate job describe ${job_UUID} +``` + +When the job status is` Success`, it indicates that the deployment is successful. +The envoy will automatically register to the director. Check the logs of both the director and envoy to see if that step succeeded. + +### Next steps +You can add more envoys with similar steps as above, with new certificate and new names. And once you have all the envoys deployed and ready, you can run the jupyter notebook as suggested in the director's document to start the training. diff --git a/helm-charts/charts/fate-exchange/.helmignore b/helm-charts/charts/fate-exchange/.helmignore new file mode 100644 index 00000000..7017e20b --- /dev/null +++ b/helm-charts/charts/fate-exchange/.helmignore @@ -0,0 +1,7 @@ +# comment +.git +*/temp* +*/*/temp* +temp? + +*exe \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/Chart.yaml b/helm-charts/charts/fate-exchange/Chart.yaml new file mode 100644 index 00000000..7aa60c28 --- /dev/null +++ b/helm-charts/charts/fate-exchange/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "exchangev1.6.1 & fedlcmv0.1.0" +description: A Helm chart for fate exchange and fml-manager +name: fate-exchange +version: v1.6.1-fedlcm-v0.1.0 diff --git a/helm-charts/charts/fate-exchange/templates/_helpers.tpl b/helm-charts/charts/fate-exchange/templates/_helpers.tpl new file mode 100644 index 00000000..5e7cd737 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/_helpers.tpl @@ -0,0 +1,41 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{/* Helm required labels */}} +{{- define "fate.labels" -}} +name: {{ .Values.partyName | quote }} +partyId: {{ .Values.partyId | quote }} +owner: kubefate +cluster: fate-exchange +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +{{- end -}} + +{{/* matchLabels */}} +{{- define "fate.matchLabels" -}} +name: {{ .Values.partyName | quote }} +partyId: {{ .Values.partyId | quote }} +{{- end -}} + +{{/* +Create the name of the controller service account to use +*/}} +{{- define "serviceAccountName" -}} +{{- if .Values.podSecurityPolicy.enabled -}} + {{ default .Values.partyName }} +{{- else -}} + {{ default "default" }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/templates/fml-manager/postgres-module.yaml b/helm-charts/charts/fate-exchange/templates/fml-manager/postgres-module.yaml new file mode 100644 index 00000000..746061b3 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/fml-manager/postgres-module.yaml @@ -0,0 +1,128 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.postgres.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: postgres +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: postgres + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.postgres.image | default "postgres" }}:{{ .Values.modules.postgres.imageTag | default "13.3"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: POSTGRES_USER + value: {{ .Values.modules.postgres.user | quote }} + - name: POSTGRES_PASSWORD + value: {{ .Values.modules.postgres.password | quote }} + - name: POSTGRES_DB + value: {{ .Values.modules.postgres.db | quote }} + ports: + - containerPort: 5432 + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-data + subPath: data + {{- with .Values.modules.postgres.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.postgres.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.postgres.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + {{- if not .Values.persistence.enabled }} + - name: postgres-data + emptyDir: {} + {{- else }} + - name: postgres-data + persistentVolumeClaim: + claimName: {{ .Values.modules.postgres.existingClaim | default "postgres-data" }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-postgres" + port: 5432 + targetPort: 5432 + {{- if eq .Values.modules.postgres.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.postgres.nodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.postgres.type }} + {{- if .Values.modules.postgres.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.postgres.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: postgres +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- if and .Values.persistence.enabled (not .Values.modules.postgres.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: postgres-data + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.postgres.accessMode }} + resources: + requests: + storage: {{ .Values.modules.postgres.size }} + {{- if .Values.modules.postgres.storageClass }} + {{- if eq "-" .Values.modules.postgres.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.modules.postgres.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{ end }} diff --git a/helm-charts/charts/fate-exchange/templates/fml-manager/server.yaml b/helm-charts/charts/fate-exchange/templates/fml-manager/server.yaml new file mode 100644 index 00000000..ce2b6790 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/fml-manager/server.yaml @@ -0,0 +1,135 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.fmlManagerServer.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fml-manager-server + labels: + fateMoudle: fml-manager-server +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: fml-manager-server +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: fml-manager-server +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: fml-manager-server + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.fmlManagerServer.image | default "fml-manager-server" }}:{{ .Values.modules.fmlManagerServer.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{ if .Values.modules.fmlManagerServer.tlsEnabled }} + - containerPort: {{ .Values.modules.fmlManagerServer.tlsPort }} + {{ else }} + - containerPort: 8080 + {{ end }} + {{ if .Values.modules.fmlManagerServer.tlsEnabled }} + volumeMounts: + - mountPath: /var/lib/fml_manager/cert + name: fml-manager-cert + {{ end }} + env: + - name: POSTGRES_HOST + value: {{ .Values.modules.fmlManagerServer.postgresHost | quote }} + - name: POSTGRES_PORT + value: {{ .Values.modules.fmlManagerServer.postgresPort | quote }} + - name: POSTGRES_USER + value: {{ .Values.modules.fmlManagerServer.postgresUser | quote }} + - name: POSTGRES_DB + value: {{ .Values.modules.fmlManagerServer.postgresDb | quote }} + - name: POSTGRES_PASSWORD + value: {{ .Values.modules.fmlManagerServer.postgresPassword | quote }} + - name: FMLMANAGER_TLS_ENABLED + value: {{ .Values.modules.fmlManagerServer.tlsEnabled | quote }} + {{ if .Values.modules.fmlManagerServer.tlsEnabled }} + - name: FMLMANAGER_TLS_SERVER_CERT + value: {{ .Values.modules.fmlManagerServer.serverCert | quote }} + - name: FMLMANAGER_TLS_SERVER_KEY + value: {{ .Values.modules.fmlManagerServer.serverKey | quote }} + - name: FMLMANAGER_TLS_CLIENT_CERT + value: {{ .Values.modules.fmlManagerServer.clientCert | quote }} + - name: FMLMANAGER_TLS_CLIENT_KEY + value: {{ .Values.modules.fmlManagerServer.clientKey | quote }} + - name: FMLMANAGER_TLS_CA_CERT + value: {{ .Values.modules.fmlManagerServer.caCert | quote }} + - name: FMLMANAGER_TLS_PORT + value: {{ .Values.modules.fmlManagerServer.tlsPort | quote }} + {{ end }} + {{- with .Values.modules.fmlManagerServer.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.fmlManagerServer.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.fmlManagerServer.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + {{ if .Values.modules.fmlManagerServer.tlsEnabled }} + volumes: + - name: fml-manager-cert + secret: + secretName: fml-manager-cert + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: fml-manager-server + labels: + fateMoudle: fml-manager-server +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "http-fml-manager-server" + port: 8080 + targetPort: 8080 + {{- if eq .Values.modules.fmlManagerServer.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.fmlManagerServer.nodePort }} + {{- end }} + protocol: TCP + - name: "https-fml-manager-server" + port: 8443 + targetPort: {{ .Values.modules.fmlManagerServer.tlsPort }} + {{- if eq .Values.modules.fmlManagerServer.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.fmlManagerServer.nodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.fmlManagerServer.type }} + {{- if .Values.modules.fmlManagerServer.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.fmlManagerServer.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: fml-manager-server +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} diff --git a/helm-charts/charts/fate-exchange/templates/nginx/nginx.yaml b/helm-charts/charts/fate-exchange/templates/nginx/nginx.yaml new file mode 100644 index 00000000..cf997390 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/nginx/nginx.yaml @@ -0,0 +1,209 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.nginx.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: nginx-config + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +data: + route_table.yaml: | + default: + proxy: + - host: nginx + http_port: 9300 + grpc_port: 9310 + {{ .Values.partyId }}: + proxy: + - host: nginx + http_port: 9300 + grpc_port: 9310 + fateflow: + - host: fateflow + http_port: 9380 + grpc_port: 9360 +{{- range $key, $val := .Values.modules.nginx.route_table }} + {{ $key }}: +{{ toYaml . | indent 6 }} +{{- end }} + nginx.conf: | + + #user nobody; + worker_processes 2; + + #error_log logs/error.log; + #error_log logs/error.log notice; + error_log /dev/stdout info; + error_log /dev/stderr error; + + #pid logs/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" "$http_host" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '$upstream_status $upstream_addr ' + '$request_time $upstream_response_time' + ; + + access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + underscores_in_headers on; + + #gzip on; + lua_package_path "$prefix/lua/?.lua;;"; + init_worker_by_lua_file 'lua/initialize.lua'; + + upstream http_cluster { + server 127.0.0.1:9300; # just an invalid address as a place holder + balancer_by_lua_file 'lua/balancer.lua'; + } + + upstream grpc_cluster { + server 127.0.0.1:9310; # just an invalid address as a place holder + balancer_by_lua_file 'lua/balancer.lua'; + } + + include vhost/*.conf; + } + + stream { + log_format tcp_proxy '$remote_addr [$time_local] ' + '$protocol $status $bytes_sent $bytes_received ' + '$session_time "$upstream_addr" ' + '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"'; + + access_log logs/tcp-access.log tcp_proxy; + + server { + listen 9128; + proxy_connect_timeout 1s; + proxy_timeout 3s; + proxy_pass 127.0.0.1:3128; + } + } + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: {{ default 1 .Values.modules.nginx.replicas }} + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: nginx +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: nginx + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.nginx.image | default "nginx" }}:{{ .Values.modules.nginx.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + - | + sed -i "s#conf/route_table.yaml#conf/modifiable/route_table.yaml#g" nginx/lua/route_table.lua; + echo "change path of route_table.yaml success!" + openresty -g 'daemon off;' + ports: + - containerPort: 9300 + - containerPort: 9310 + volumeMounts: + - mountPath: /data/projects/fate/proxy/nginx/conf/nginx.conf + name: nginx-confs + subPath: nginx.conf + - mountPath: /data/projects/fate/proxy/nginx/conf/modifiable/ + name: nginx-confs + {{- with .Values.modules.nginx.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.nginx.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.nginx.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: nginx-confs + configMap: + name: nginx-config +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "http" + port: 9300 + targetPort: 9300 + {{- if eq .Values.modules.nginx.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.nginx.httpNodePort }} + {{- end }} + protocol: TCP + - name: "grpc" + port: 9310 + targetPort: 9310 + {{- if eq .Values.modules.nginx.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.nginx.grpcNodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.nginx.type }} + {{- if .Values.modules.nginx.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.nginx.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: nginx +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} diff --git a/helm-charts/charts/fate-exchange/templates/psp.yaml b/helm-charts/charts/fate-exchange/templates/psp.yaml new file mode 100644 index 00000000..447017ae --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/psp.yaml @@ -0,0 +1,35 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + labels: + fateMoudle: psp +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/templates/role.yaml b/helm-charts/charts/fate-exchange/templates/role.yaml new file mode 100644 index 00000000..ba1363c7 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/role.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + fateMoudle: role +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +rules: + - apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ .Values.partyName }}] +{{- end }} diff --git a/helm-charts/charts/fate-exchange/templates/rolebinding.yaml b/helm-charts/charts/fate-exchange/templates/rolebinding.yaml new file mode 100644 index 00000000..df87f27c --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/rolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + fateMoudle: serviceAccount +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Values.partyName }} +subjects: + - kind: ServiceAccount + name: {{ .Values.partyName }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm-charts/charts/fate-exchange/templates/serviceaccount.yaml b/helm-charts/charts/fate-exchange/templates/serviceaccount.yaml new file mode 100644 index 00000000..4a1f9a58 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/serviceaccount.yaml @@ -0,0 +1,23 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + fateMoudle: serviceAccount +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +{{- end }} diff --git a/helm-charts/charts/fate-exchange/templates/traffic-server/traffic-server-module.yaml b/helm-charts/charts/fate-exchange/templates/traffic-server/traffic-server-module.yaml new file mode 100644 index 00000000..02cce192 --- /dev/null +++ b/helm-charts/charts/fate-exchange/templates/traffic-server/traffic-server-module.yaml @@ -0,0 +1,176 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.trafficServer.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: traffic-server-config + labels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + owner: kubefate + cluster: fate-exchange +data: + ssl_multicert.config: | + dest_ip=* ssl_cert_name=proxy.cert.pem ssl_key_name=proxy.key.pem + records.config: | + CONFIG proxy.config.http.cache.http INT 0 + CONFIG proxy.config.reverse_proxy.enabled INT 0 + CONFIG proxy.config.url_remap.remap_required INT 0 + CONFIG proxy.config.url_remap.pristine_host_hdr INT 0 + CONFIG proxy.config.http.response_server_enabled INT 0 + + CONFIG proxy.config.http.server_ports STRING 8080 8080:ipv6 443:ssl + + CONFIG proxy.config.http.connect_ports STRING 443 6650-6700 30000-33000 + + CONFIG proxy.config.ssl.CA.cert.filename STRING ca.cert.pem + CONFIG proxy.config.ssl.CA.cert.path STRING /opt/proxy + + CONFIG proxy.config.ssl.server.cert.path STRING /opt/proxy + + CONFIG proxy.config.ssl.servername.filename STRING sni/sni.yaml +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: traffic-server-sni + labels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + owner: kubefate + cluster: fate-exchange +data: + sni.yaml: | + sni: + {{- range .Values.modules.trafficServer.route_table.sni }} + - fqdn: {{ .fqdn }} + tunnel_route: {{ .tunnelRoute }} + {{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: traffic-server + labels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + owner: kubefate + cluster: fate-exchange +spec: + replicas: {{ default 1 .Values.modules.trafficServer.replicas }} + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + template: + metadata: + labels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + owner: kubefate + cluster: fate-exchange + spec: + containers: + - image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.trafficServer.image | default "trafficserver" }}:{{ .Values.modules.trafficServer.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: traffic-server + command: + - /bin/bash + - -c + - /opt/trafficserver/bin/traffic_manager + ports: + - containerPort: 8080 + - containerPort: 443 + volumeMounts: + - mountPath: /etc/trafficserver/records.config + name: traffic-server-confs + subPath: records.config + - mountPath: /etc/trafficserver/ssl_multicert.config + name: traffic-server-confs + subPath: ssl_multicert.config + - mountPath: /opt/trafficserver/etc/trafficserver/sni + name: traffic-server-sni + - mountPath: /opt/proxy + name: traffic-server-cert-file + - mountPath: /opt/trafficserver/etc/trafficserver/proxy.cert.pem + name: traffic-server-cert-file + subPath: proxy.cert.pem + - mountPath: /opt/trafficserver/etc/trafficserver/proxy.key.pem + name: traffic-server-cert-file + subPath: proxy.key.pem + {{- with .Values.modules.trafficServer.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.trafficServer.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.trafficServer.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: traffic-server-confs + configMap: + name: traffic-server-config + - name: traffic-server-sni + configMap: + name: traffic-server-sni + - name: traffic-server-cert-file + secret: + secretName: traffic-server-cert +--- +apiVersion: v1 +kind: Service +metadata: + labels: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} + owner: kubefate + cluster: fate-exchange + name: traffic-server +spec: + ports: + - name: "443" + port: 443 + targetPort: 443 + nodePort: {{ .Values.modules.trafficServer.nodePort }} + protocol: TCP + type: {{ .Values.modules.trafficServer.type }} + {{- if .Values.modules.trafficServer.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.trafficServer.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: traffic-server + name: {{ .Values.partyName | quote }} + partyId: {{ .Values.partyId | quote }} +--- +{{ end }} \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/values-template-example.yaml b/helm-charts/charts/fate-exchange/values-template-example.yaml new file mode 100644 index 00000000..718fb22c --- /dev/null +++ b/helm-charts/charts/fate-exchange/values-template-example.yaml @@ -0,0 +1,104 @@ +name: fate-exchange +namespace: fate-exchange +chartName: fate-exchange +chartVersion: v1.6.1-fedlcm-v0.1.0 +partyId: 0 +registry: "" +pullPolicy: +imagePullSecrets: +- name: myregistrykey +persistence: false +istio: + enabled: false +podSecurityPolicy: + enabled: false +modules: + - trafficServer + - nginx + - postgres + - fmlManagerServer + +# trafficServer: + # image: federatedai/trafficServer + # imageTag: latest + # replicas: 3 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # nodePort: 30007 + # loadBalancerIP: 192.168.0.1 + # route_table: + # sni: + # - fqdn: 10000.fate.org + # tunnelRoute: 192.168.0.2:30109 + # - fqdn: 9999.fate.org + # tunnelRoute: 192.168.0.3:30099 + +# nginx: + # image: federatedai/nginx + # imageTag: 1.6.1-release + # replicas: 3 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # httpNodePort: 30003 + # grpcNodePort: 30008 + # loadBalancerIP: 192.168.0.1 + # route_table: + # 9999: + # proxy: + # - host: 192.168.9.1 + # http_port: 30093 + # grpc_port: 30098 + # fateflow: + # - host: 192.168.9.1 + # http_port: 30097 + # grpc_port: 30092 + # 10000: + # proxy: + # - host: 192.168.10.1 + # http_port: 30103 + # grpc_port: 30108 + # fateflow: + # - host: 192.168.10.1 + # http_port: 30107 + # grpc_port: 30102 + +# postgres: + # image: postgres + # imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + # user: fml_manager + # password: fml_manager + # db: fml_manager + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + +# fmlManagerServer: + # image: federatedai/fml-manager-server + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # nodePort: + # loadBalancerIP: 192.168.0.1 + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: fml_manager + # postgresUser: fml_manager + # postgresPassword: fml_manager + # tlsPort: 8443 + # serverCert: /var/lib/fml_manager/cert/server.crt + # serverKey: /var/lib/fml_manager/cert/server.key + # clientCert: /var/lib/fml_manager/cert/client.crt + # clientKey: /var/lib/fml_manager/cert/client.key + # caCert: /var/lib/fml_manager/cert/ca.crt + # tlsEnabled: 'true' \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/values-template.yaml b/helm-charts/charts/fate-exchange/values-template.yaml new file mode 100644 index 00000000..edaaf846 --- /dev/null +++ b/helm-charts/charts/fate-exchange/values-template.yaml @@ -0,0 +1,135 @@ +partyId: {{ .partyId }} +partyName: {{ .name }} + +image: + registry: {{ .registry | default "" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + + +modules: + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + replicas: {{ .replicas }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + trafficServer: + include: {{ has "trafficServer" .modules }} + {{- with .trafficServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + replicas: {{ .replicas }} + nodePort: {{ .nodePort }} + route_table: + sni: + {{- range .route_table.sni }} + - fqdn: {{ .fqdn }} + tunnelRoute: {{ .tunnelRoute }} + {{- end }} + {{- end }} + + postgres: + include: {{ has "postgres" .modules }} + {{- with .postgres }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type | default "ClusterIP" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + user: {{ .user }} + password: {{ .password }} + db: {{ .db }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode }} + size: {{ .size }} + {{- end }} + + fmlManagerServer: + include: {{ has "fmlManagerServer" .modules }} + {{- with .fmlManagerServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + postgresHost: {{ .postgresHost | default "postgres" }} + postgresPort: {{ .postgresPort | default "5432" }} + postgresDb: {{ .postgresDb | default "fml_manager" }} + postgresUser: {{ .postgresUser | default "fml_manager" }} + postgresPassword: {{ .postgresPassword | default "fml_manager" }} + tlsPort: {{ .tlsPort | default "8443" }} + serverCert: {{ .serverCert | default "/var/lib/fml_manager/cert/server.crt" }} + serverKey: {{ .serverKey | default "/var/lib/fml_manager/cert/server.key" }} + clientCert: {{ .clientCert| default "/var/lib/fml_manager/cert/client.crt" }} + clientKey: {{ .clientKey | default "/var/lib/fml_manager/cert/client.key" }} + caCert: {{ .caCert | default "/var/lib/fml_manager/cert/ca.crt" }} + tlsEnabled: {{ .tlsEnabled | default "true" }} + {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/fate-exchange/values.yaml b/helm-charts/charts/fate-exchange/values.yaml new file mode 100644 index 00000000..2fbad795 --- /dev/null +++ b/helm-charts/charts/fate-exchange/values.yaml @@ -0,0 +1,103 @@ +partyId: 1 +partyName: fate-exchange + +image: + registry: + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +persistence: + enabled: false + +modules: + nginx: + include: true + image: federatedai/nginx + imageTag: 1.6.1-release + type: ClusterIP + # httpNodePort: 30003 + # grpcNodePort: 30008 + # loadBalancerIP: + # nodeSelector: + # tolerations: + # affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + # 10000: + # fateflow: + # - grpc_port: 30102 + # host: 192.168.10.1 + # http_port: 30107 + # proxy: + # - grpc_port: 30108 + # host: 192.168.10.1 + # http_port: 30103 + # 9999: + # fateflow: + # - grpc_port: 30092 + # host: 192.168.9.1 + # http_port: 30097 + # proxy: + # - grpc_port: 30098 + # host: 192.168.9.1 + # http_port: 30093 + trafficServer: + image: federatedai/trafficserver + imageTag: latest + include: true + type: ClusterIP + # nodePort: 30007 + # loadBalancerIP: + # nodeSelector: + # tolerations: + # affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + sni: + # - fqdn: 10000.fate.org + # tunnelRoute: 192.168.0.2:30109 + # - fqdn: 9999.fate.org + # tunnelRoute: 192.168.0.3:30099 + postgres: + include: true + type: ClusterIP + image: postgres + imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + user: fml_manager + password: fml_manager + db: fml_manager + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + + fmlManagerServer: + include: true + image: federatedai/fml-manager-server + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: 192.168.0.1 + postgresHost: postgres + postgresPort: 5432 + postgresDb: fml_manager + postgresUser: fml_manager + postgresPassword: fml_manager + tlsPort: 8443 + serverCert: /var/lib/fml_manager/cert/server.crt + serverKey: /var/lib/fml_manager/cert/server.key + clientCert: /var/lib/fml_manager/cert/client.crt + clientKey: /var/lib/fml_manager/cert/client.key + caCert: /var/lib/fml_manager/cert/ca.crt + tlsEnabled: 'true' \ No newline at end of file diff --git a/helm-charts/charts/fate/.helmignore b/helm-charts/charts/fate/.helmignore new file mode 100644 index 00000000..7017e20b --- /dev/null +++ b/helm-charts/charts/fate/.helmignore @@ -0,0 +1,7 @@ +# comment +.git +*/temp* +*/*/temp* +temp? + +*exe \ No newline at end of file diff --git a/helm-charts/charts/fate/Chart.yaml b/helm-charts/charts/fate/Chart.yaml new file mode 100644 index 00000000..df83d490 --- /dev/null +++ b/helm-charts/charts/fate/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +appVersion: "fatev1.6.1+fedlcmv0.1.0" +description: Helm chart for FATE and site-portal in FedLCM +name: fate +version: v1.6.1-fedlcm-v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git + - https://github.com/FederatedAI/FATE.git diff --git a/helm-charts/charts/fate/templates/NOTES.txt b/helm-charts/charts/fate/templates/NOTES.txt new file mode 100644 index 00000000..97515e6a --- /dev/null +++ b/helm-charts/charts/fate/templates/NOTES.txt @@ -0,0 +1,20 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# optional +If you have configured ingress or isto, you may need to add " [ , ...]"to the hosts. +{{- with .Values.ingress.frontend.hosts }} +Site Portal UI +{{ toYaml . | indent 2 }} +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/_helpers.tpl b/helm-charts/charts/fate/templates/_helpers.tpl new file mode 100644 index 00000000..1cb428e7 --- /dev/null +++ b/helm-charts/charts/fate/templates/_helpers.tpl @@ -0,0 +1,41 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{/* Helm required labels */}} +{{- define "fate.labels" -}} +name: {{ .Values.partyName | quote }} +partyId: {{ .Values.partyId | quote }} +owner: kubefate +cluster: fate +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +{{- end -}} + +{{/* matchLabels */}} +{{- define "fate.matchLabels" -}} +name: {{ .Values.partyName | quote }} +partyId: {{ .Values.partyId | quote }} +{{- end -}} + +{{/* +Create the name of the controller service account to use +*/}} +{{- define "serviceAccountName" -}} +{{- if .Values.podSecurityPolicy.enabled -}} + {{ default .Values.partyName }} +{{- else -}} + {{ default "default" }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/fateflow/python-spark.yaml b/helm-charts/charts/fate/templates/fateflow/python-spark.yaml new file mode 100644 index 00000000..d9d03901 --- /dev/null +++ b/helm-charts/charts/fate/templates/fateflow/python-spark.yaml @@ -0,0 +1,537 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.python.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: python-config + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +data: + spark-defaults.conf: | + spark.master {{ .Values.modules.python.spark.master | default "spark://spark-master:7077"}} + spark.driver.host {{ .Values.modules.python.spark.driverHost | default "fateflow" }} + {{- if .Values.modules.python.spark.driverStartPort }} + spark.driver.port {{ .Values.modules.python.spark.driverStartPort }} + {{- end }} + + {{- if .Values.modules.python.spark.portMaxRetries }} + spark.port.maxRetries {{ .Values.modules.python.spark.portMaxRetries }} + {{- end }} + + {{- if .Values.modules.python.spark.blockManagerStartPort }} + spark.blockManager.port {{ .Values.modules.python.spark.blockManagerStartPort }} + {{- end }} + + {{- if .Values.modules.python.spark.blockManagerStartPort }} + spark.driver.bindAddress 0.0.0.0 + {{- end }} + + {{- if .Values.modules.python.spark.pysparkPython }} + spark.pyspark.python {{ .Values.modules.python.spark.pysparkPython }} + spark.pyspark.driver.python python + {{- end }} + service_conf.yaml: | + work_mode: 1 + use_registry: {{ .Values.modules.serving.useRegistry | default false }} + use_deserialize_safe_module: false + fateflow: + # you must set real ip address, 127.0.0.1 and 0.0.0.0 is not supported + host: {{ if .Values.istio.enabled }}127.0.0.1{{ else }}fateflow{{ end }} + http_port: 9380 + grpc_port: 9360 + # support rollsite/nginx/fateflow as a coordination proxy + # rollsite support fate on eggroll, use grpc protocol + # nginx support fate on eggroll and fate on spark, use http or grpc protocol, default is http + # fateflow support fate on eggroll and fate on spark, use http protocol, but not support exchange network mode + + # format(proxy: rollsite) means rollsite use the rollsite configuration of fate_one_eggroll and nginx use the nginx configuration of fate_one_spark + # you also can customize the config like this(set fateflow of the opposite party as proxy): + # proxy: + # name: fateflow + # host: xx + # http_port: xx + # grpc_port: xx + {{- if eq .Values.modules.python.backend "spark" }} + proxy: nginx + {{- else }} + proxy: rollsite + {{- end }} + # support default/http/grpc + protocol: default + # It can also be specified in the job configuration using the federated_status_collect_type parameter + default_federated_status_collect_type: PULL + fateboard: + host: fateboard + port: 8080 + database: + name: '{{ .Values.externalMysqlDatabase | default .Values.modules.mysql.database | default "eggroll_meta" }}' + user: '{{ .Values.externalMysqlUser | default .Values.modules.mysql.user | default "fate" }}' + passwd: '{{ .Values.externalMysqlPassword | default .Values.modules.mysql.password | default "fate_dev" }}' + host: '{{ .Values.externalMysqlIp | default .Values.modules.mysql.ip | default "mysql" }}' + port: {{ .Values.externalMysqlPort | default .Values.modules.mysql.port | default "3306" }} + max_connections: 100 + stale_timeout: 30 + fate_on_eggroll: + clustermanager: + cores_per_node: {{ .Values.modules.python.clustermanager.cores_per_node | default 16 }} + nodes: {{ .Values.modules.python.clustermanager.nodes | default 2 }} + rollsite: + host: rollsite + port: 9370 + fate_on_spark: + spark: + # default use SPARK_HOME environment variable + home: + cores_per_node: {{ .Values.modules.python.spark.cores_per_node }} + nodes: {{ .Values.modules.python.spark.nodes }} + hdfs: + name_node: {{ .Values.modules.python.hdfs.name_node | default "hdfs://namenode:9000" }} + # default / + path_prefix: {{ .Values.modules.python.hdfs.path_prefix }} + rabbitmq: + host: {{ .Values.modules.python.rabbitmq.host }} + mng_port: {{ .Values.modules.python.rabbitmq.mng_port }} + port: {{ .Values.modules.python.rabbitmq.port }} + user: {{ .Values.modules.python.rabbitmq.user }} + password: {{ .Values.modules.python.rabbitmq.password }} + # default conf/rabbitmq_route_table.yaml + route_table: conf/rabbitmq_route_table/rabbitmq_route_table.yaml + pulsar: + host: {{ .Values.modules.python.pulsar.host }} + port: {{ .Values.modules.python.pulsar.port }} + mng_port: {{ .Values.modules.python.pulsar.mng_port }} + topic_ttl: 3 + # default conf/pulsar_route_table.yaml + route_table: conf/pulsar_route_table/pulsar_route_table.yaml + nginx: + host: {{ .Values.modules.python.nginx.host }} + http_port: {{ .Values.modules.python.nginx.http_port }} + grpc_port: {{ .Values.modules.python.nginx.grpc_port }} + model_store_address: + storage: mysql + name: {{ .Values.externalMysqlDatabase | default .Values.modules.mysql.database | default "eggroll_meta" }} + host: '{{ .Values.externalMysqlIp | default .Values.modules.mysql.ip | default "mysql" }}' + port: {{ .Values.externalMysqlPort | default .Values.modules.mysql.port | default "3306" }} + user: '{{ .Values.externalMysqlUser | default .Values.modules.mysql.user | default "fate" }}' + passwd: '{{ .Values.externalMysqlPassword | default .Values.modules.mysql.password | default "fate_dev" }}' + max_connections: 10 + stale_timeout: 10 + {{- with .Values.modules.serving }} + servings: + hosts: + {{- if and .ip .port}} + - '{{ .ip }}:{{ .port }}' + {{- else }} + - '' + {{- end }} + {{- if and .useRegistry .zookeeper }} + zookeeper: +{{ toYaml .zookeeper | indent 6 }} + {{- end }} + {{- end }} + transfer_conf.yaml: | + paths: # dir or path + - "python/federatedml/transfer_variable/auth_conf" +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: fateboard-config + labels: + fateMoudle: fateboard +{{ include "fate.labels" . | indent 4 }} +data: + application.properties: | + server.port=8080 + fateflow.url=http://{{ if .Values.istio.enabled }}127.0.0.1{{ else }}fateflow{{ end }}:9380 + spring.datasource.driver-Class-Name=com.mysql.cj.jdbc.Driver + spring.http.encoding.charset=UTF-8 + spring.http.encoding.enabled=true + server.tomcat.uri-encoding=UTF-8 + fateboard.datasource.jdbc-url=jdbc:mysql://{{ .Values.externalMysqlIp | default .Values.modules.mysql.ip | default "mysql" }}:{{ .Values.externalMysqlPort | default .Values.modules.mysql.port | default "3306" }}/{{ .Values.externalMysqlDatabase | default .Values.modules.mysql.database | default "eggroll_meta" }}?characterEncoding=utf8&characterSetResults=utf8&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8 + fateboard.datasource.username={{ .Values.externalMysqlUser | default .Values.modules.mysql.user | default "fate" }} + fateboard.datasource.password={{ .Values.externalMysqlPassword | default .Values.modules.mysql.password | default "fate_dev" }} + server.tomcat.max-threads=1000 + server.tomcat.max-connections=20000 + spring.servlet.multipart.max-file-size=10MB + spring.servlet.multipart.max-request-size=100MB + spring.datasource.druid.filter.config.enabled=false + spring.datasource.druid.web-stat-filter.enabled=false + spring.datasource.druid.stat-view-servlet.enabled=false + server.compression.enabled=true + server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain + server.board.login.username={{ .Values.modules.fateboard.username }} + server.board.login.password={{ .Values.modules.fateboard.password }} + #server.ssl.key-store=classpath: + #server.ssl.key-store-password= + #server.ssl.key-password= + #server.ssl.key-alias= +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: pulsar-route-table + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +data: + pulsar_route_table.yaml: | + {{- with .Values.modules.pulsar.exchange }} + default: + proxy: "{{ .ip }}:{{ .port }}" + domain: "{{ .domain }}" + {{- end }} +{{- if .Values.modules.pulsar.route_table }} +{{- range $key, $val := .Values.modules.pulsar.route_table }} + {{ $key }}: +{{ toYaml . | indent 6 }} +{{- end }} +{{- else }} + {{ .Values.partyId }}: + host: pulsar + port: 6650 + sslPort: 6651 + proxy: "" +{{- end}} +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: rabbitmq-route-table + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +data: + rabbitmq_route_table.yaml: | +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: python + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: python +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 8 }} + spec: + {{- if .Values.istio.enabled }} + {{- else }} + initContainers: + - image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.mysql.image | default "mysql" }}:{{ .Values.modules.mysql.imageTag | default "8" }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: ping-mysql + env: + - name: MYSQL_DATABASE + value: "{{ .Values.externalMysqlDatabase | default .Values.modules.mysql.database | default "eggroll_meta" }}" + - name: MYSQL_USER + value: "{{ .Values.externalMysqlUser | default .Values.modules.mysql.user | default "fate" }}" + - name: MYSQL_PASSWORD + value: "{{ .Values.externalMysqlPassword | default .Values.modules.mysql.password | default "fate_dev" }}" + - name: MYSQL_HOST + value: "{{ .Values.externalMysqlIp | default .Values.modules.mysql.ip | default "mysql" }}" + - name: MYSQL_PORT + value: "{{ .Values.externalMysqlPort | default .Values.modules.mysql.port | default "3306" | }}" + command: + - /bin/bash + - -c + - | + set -x + function checkMySQL(){ + checkMySQLCount=0 + while true ; do + checkMySQLCount=$[checkMySQLCount+1] + echo "Waiting for mysql started. check count: $checkMySQLCount" + sleep 2 + + state=`mysqladmin ping -h ${MYSQL_HOST} --port=${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD}| awk '{print $3}'` + if [ "$state" == "alive" ]; then + echo "mysql server has been already started." + break + fi + done + } + echo "Waiting for mysql started..." + checkMySQL + {{- end }} + containers: + - name: python + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.python.image | default "python-spark" }}:{{ .Values.modules.python.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.modules.python.resources}} + resources: + {{- range $key, $val := .Values.modules.python.resources }} + {{ $key }}: +{{ toYaml $val | indent 14 }} + {{- end }} + {{- end }} + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if eq .Values.modules.python.backend "spark" }} + - name: FATE_FLOW_UPLOAD_MAX_NUM + value: "1000000" + - name: FATE_FLOW_UPLOAD_MAX_BYTES + value: "104868093952" + {{- end }} + ports: + - containerPort: 9360 + - containerPort: 9380 + command: + - /bin/bash + - -c + - | + set -x + mkdir -p /data/projects/fate/conf/ + cp /data/projects/fate/conf1/transfer_conf.yaml /data/projects/fate/conf/transfer_conf.yaml + cp /data/projects/fate/conf1/service_conf.yaml /data/projects/fate/conf/service_conf.yaml + # cp /data/projects/fate/conf1/pipeline_conf.yaml /data/projects/fate/conf/pipeline_conf.yaml + # fix fateflow conf must use IP + sed -i "s/host: fateflow/host: ${POD_IP}/g" /data/projects/fate/conf/service_conf.yaml + # fix pipeline conf must use IP + # sed -i "s/ip: fateflow/ip: ${POD_IP}/g" /data/projects/fate/conf/pipeline_conf.yaml + + cp /data/projects/spark-2.4.1-bin-hadoop2.7/conf/spark-defaults-template.conf /data/projects/spark-2.4.1-bin-hadoop2.7/conf/spark-defaults.conf + sed -i "s/fateflow/${POD_IP}/g" /data/projects/spark-2.4.1-bin-hadoop2.7/conf/spark-defaults.conf + + sleep 5; + python ./fate_flow/fate_flow_server.py + volumeMounts: + - name: python-data + mountPath: /data/projects/fate/logs + subPath: logs + - mountPath: /data/projects/fate/conf1/ + name: python-confs + - mountPath: /data/projects/spark-2.4.1-bin-hadoop2.7/conf/spark-defaults-template.conf + name: python-confs + subPath: spark-defaults.conf + - mountPath: /data/projects/fate/conf/rabbitmq_route_table + name: rabbitmq-route-table + - mountPath: /data/projects/fate/conf/pulsar_route_table + name: pulsar-route-table + # TODO add mountPath of job and model-cache + - mountPath: /data/projects/fate/jobs + name: python-data + subPath: jobs + - mountPath: /data/projects/fate/model_local_cache + name: python-data + subPath: model-local-cache + {{- if .Values.modules.fateboard.include }} + - image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.fateboard.image | default "fateboard" }}:{{ .Values.modules.fateboard.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: fateboard + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /data/projects/fate/fateboard/conf/application.properties + name: fateboard-confs + subPath: application.properties + - name: python-data + mountPath: /data/projects/fate/logs + subPath: logs + {{- end }} + {{- with .Values.modules.python.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.python.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.python.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} +{{- if .Values.modules.python.serviceAccountName }} + serviceAccountName: {{ .Values.modules.python.serviceAccountName }} +{{- else }} + serviceAccountName: {{ template "serviceAccountName" . }} +{{- end }} + restartPolicy: Always + volumes: + - name: eggroll-confs + configMap: + name: eggroll-config + - name: python-confs + configMap: + name: python-config + - name: rabbitmq-route-table + configMap: + name: rabbitmq-route-table + - name: pulsar-route-table + configMap: + name: pulsar-route-table + - name: fateboard-confs + configMap: + name: fateboard-config + {{- if not .Values.persistence.enabled }} + - name: python-data + emptyDir: {} + {{- else }} + - name: python-data + persistentVolumeClaim: + claimName: {{ .Values.modules.python.existingClaim | default "python-data" }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: fateflow + labels: + fateMoudle: fateflow +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-grpc" + port: 9360 + targetPort: 9360 + protocol: TCP + - name: "tcp-http" + port: 9380 + targetPort: 9380 + protocol: TCP + type: ClusterIP + selector: + fateMoudle: python +{{ include "fate.matchLabels" . | indent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: fateflow-client + labels: + fateMoudle: fateflow +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-grpc" + port: 9360 + targetPort: 9360 + {{- if eq .Values.modules.python.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.python.grpcNodePort }} + {{- end }} + protocol: TCP + - name: "tcp-http" + port: 9380 + targetPort: 9380 + {{- if eq .Values.modules.python.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.python.httpNodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.python.type }} + + {{- if .Values.modules.python.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.python.loadBalancerIP }}" + {{- end }} + + selector: + fateMoudle: python +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- if and .Values.modules.python.spark.portMaxRetries (ne (print .Values.modules.python.spark.driverHost) "fateflow") }} +apiVersion: v1 +kind: Service +metadata: + name: fateflow-sparkdriver + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + {{ $spark := .Values.modules.python.spark }} + {{- range .Values.modules.python.spark.portMaxRetries | int | until }} + - name: "spark-driver-port-{{ . }}" + port: {{ $spark.driverStartPort | int | add . }} + targetPort: {{ $spark.driverStartPort | int | add . }} + {{- if eq $spark.driverHostType "NodePort" "LoadBalancer" }} + nodePort: {{ $spark.driverStartPort | int | add . }} + {{- end }} + protocol: TCP + - name: "spark-block-manager-port-{{ . }}" + port: {{ $spark.blockManagerStartPort | int | add . }} + targetPort: {{ $spark.blockManagerStartPort | int | add . }} + {{- if eq $spark.driverHostType "NodePort" "LoadBalancer" }} + nodePort: {{ $spark.blockManagerStartPort | int | add . }} + {{- end }} + protocol: TCP + {{- end }} + type: {{ .Values.modules.python.spark.driverHostType }} + selector: + fateMoudle: python +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- end }} + +{{- if .Values.modules.fateboard.include }} +apiVersion: v1 +kind: Service +metadata: + name: fateboard + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-fateboard" + port: 8080 + targetPort: 8080 + protocol: TCP + type: {{ .Values.modules.fateboard.type }} + selector: + fateMoudle: python +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- end }} +{{- if and .Values.persistence.enabled (not .Values.modules.python.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: python-data + labels: + fateMoudle: python +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.python.accessMode }} + resources: + requests: + storage: {{ .Values.modules.python.size }} + {{- if .Values.modules.python.storageClass }} + {{- if eq "-" .Values.modules.python.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.modules.python.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/hdfs/hdfs-dn.yaml b/helm-charts/charts/fate/templates/hdfs/hdfs-dn.yaml new file mode 100644 index 00000000..56e118bf --- /dev/null +++ b/helm-charts/charts/fate/templates/hdfs/hdfs-dn.yaml @@ -0,0 +1,178 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.hdfs.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: datanode-env + labels: + fateMoudle: datanode +{{ include "fate.labels" . | indent 4 }} +data: + CORE_CONF_fs_defaultFS: "hdfs://namenode:9000" + CORE_CONF_hadoop_http_staticuser_user: "root" + CORE_CONF_hadoop_proxyuser_hue_hosts: "*" + CORE_CONF_hadoop_proxyuser_hue_groups: "*" + CORE_CONF_io_compression_codecs: org.apache.hadoop.io.compress.SnappyCodec + HDFS_CONF_dfs_webhdfs_enabled: 'true' + HDFS_CONF_dfs_permissions_enabled: 'false' + HDFS_CONF_dfs_namenode_datanode_registration_ip___hostname___check: 'false' + YARN_CONF_yarn_log___aggregation___enable: 'true' + YARN_CONF_yarn_log_server_url: "http://historyserver:8188/applicationhistory/logs/" + YARN_CONF_yarn_resourcemanager_recovery_enabled: 'true' + YARN_CONF_yarn_resourcemanager_store_class: org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore + YARN_CONF_yarn_resourcemanager_scheduler_class: org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler + YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___mb: '8192' + YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___vcores: '4' + YARN_CONF_yarn_resourcemanager_fs_state___store_uri: /rmstate + YARN_CONF_yarn_resourcemanager_system___metrics___publisher_enabled: 'true' + YARN_CONF_yarn_resourcemanager_hostname: resourcemanager + YARN_CONF_yarn_resourcemanager_address: resourcemanager:8032 + YARN_CONF_yarn_resourcemanager_scheduler_address: resourcemanager:8030 + YARN_CONF_yarn_resourcemanager_resource__tracker_address: resourcemanager:8031 + YARN_CONF_yarn_timeline___service_enabled: 'true' + YARN_CONF_yarn_timeline___service_generic___application___history_enabled: 'true' + YARN_CONF_yarn_timeline___service_hostname: historyserver + YARN_CONF_mapreduce_map_output_compress: 'true' + YARN_CONF_mapred_map_output_compress_codec: org.apache.hadoopac.io.compress.SnappyCodec + YARN_CONF_yarn_nodemanager_resource_memory___mb: '16384' + YARN_CONF_yarn_nodemanager_resource_cpu___vcores: '8' + YARN_CONF_yarn_nodemanager_disk___health___checker_max___disk___utilization___per___disk___percentage: '98.5' + YARN_CONF_yarn_nodemanager_remote___app___log___dir: /app-logs + YARN_CONF_yarn_nodemanager_aux___services: mapreduce_shuffle + MAPRED_CONF_mapreduce_framework_name: yarn + MAPRED_CONF_mapred_child_java_opts: '-Xmx4096m' + MAPRED_CONF_mapreduce_map_memory_mb: '4096' + MAPRED_CONF_mapreduce_reduce_memory_mb: '8192' + MAPRED_CONF_mapreduce_map_java_opts: '-Xmx3072m' + MAPRED_CONF_mapreduce_reduce_java_opts: '-Xmx6144m' + MAPRED_CONF_yarn_app_mapreduce_am_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ + MAPRED_CONF_mapreduce_map_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ + MAPRED_CONF_mapreduce_reduce_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: datanode + labels: + fateMoudle: datanode +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: datanode +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: datanode +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: datanode + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.hdfs.datanode.image | default "hadoop-datanode" }}:{{ .Values.modules.hdfs.datanode.imageTag | default "2.0.0-hadoop2.7.4-java8"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: SERVICE_PRECONDITION + value: "namenode:9000" + envFrom: + - configMapRef: + name: datanode-env + ports: + - containerPort: 9000 + - containerPort: 9870 + - containerPort: 50070 + volumeMounts: + - name: dfs + mountPath: /hadoop/dfs/data + {{- with .Values.modules.hdfs.datanode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.hdfs.datanode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.hdfs.datanode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: dfs + {{ if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.modules.hdfs.datanode.existingClaim | default "datanode-data" }} + {{ else }} + emptyDir: {} + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: datanode + labels: + fateMoudle: datanode +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-9000" + port: 9000 + targetPort: 9000 + protocol: TCP + - name: "tcp-9870" + port: 9870 + targetPort: 9870 + protocol: TCP + - name: "tcp-50070" + port: 50070 + targetPort: 50070 + protocol: TCP + type: {{ .Values.modules.hdfs.datanode.type }} + selector: + fateMoudle: datanode +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ if and .Values.persistence.enabled (not .Values.modules.hdfs.datanode.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: datanode-data + labels: + fateMoudle: datanode +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.hdfs.datanode.accessMode }} + resources: + requests: + storage: {{ .Values.modules.hdfs.datanode.size }} + {{ if .Values.modules.hdfs.datanode.storageClass }} + {{ if eq "-" .Values.modules.hdfs.datanode.storageClass }} + storageClassName: "" + {{ else }} + storageClassName: {{ .Values.modules.hdfs.datanode.storageClass }} + {{ end }} + {{ end }} +{{ end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/hdfs/hdfs-nn.yaml b/helm-charts/charts/fate/templates/hdfs/hdfs-nn.yaml new file mode 100644 index 00000000..8109b822 --- /dev/null +++ b/helm-charts/charts/fate/templates/hdfs/hdfs-nn.yaml @@ -0,0 +1,212 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.hdfs.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: namenode-config + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 4 }} +data: + core-site.xml: | + + + fs.default.name + hdfs://0.0.0.0:9000 + + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: namenode-env + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 4 }} +data: + CORE_CONF_fs_defaultFS: "hdfs://namenode:9000" + CORE_CONF_hadoop_http_staticuser_user: "root" + CORE_CONF_hadoop_proxyuser_hue_hosts: "*" + CORE_CONF_hadoop_proxyuser_hue_groups: "*" + CORE_CONF_io_compression_codecs: org.apache.hadoop.io.compress.SnappyCodec + HDFS_CONF_dfs_webhdfs_enabled: 'true' + HDFS_CONF_dfs_permissions_enabled: 'false' + HDFS_CONF_dfs_namenode_datanode_registration_ip___hostname___check: 'false' + YARN_CONF_yarn_log___aggregation___enable: 'true' + YARN_CONF_yarn_log_server_url: "http://historyserver:8188/applicationhistory/logs/" + YARN_CONF_yarn_resourcemanager_recovery_enabled: 'true' + YARN_CONF_yarn_resourcemanager_store_class: org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore + YARN_CONF_yarn_resourcemanager_scheduler_class: org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler + YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___mb: '8192' + YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___vcores: '4' + YARN_CONF_yarn_resourcemanager_fs_state___store_uri: /rmstate + YARN_CONF_yarn_resourcemanager_system___metrics___publisher_enabled: 'true' + YARN_CONF_yarn_resourcemanager_hostname: resourcemanager + YARN_CONF_yarn_resourcemanager_address: resourcemanager:8032 + YARN_CONF_yarn_resourcemanager_scheduler_address: resourcemanager:8030 + YARN_CONF_yarn_resourcemanager_resource__tracker_address: resourcemanager:8031 + YARN_CONF_yarn_timeline___service_enabled: 'true' + YARN_CONF_yarn_timeline___service_generic___application___history_enabled: 'true' + YARN_CONF_yarn_timeline___service_hostname: historyserver + YARN_CONF_mapreduce_map_output_compress: 'true' + YARN_CONF_mapred_map_output_compress_codec: org.apache.hadoopac.io.compress.SnappyCodec + YARN_CONF_yarn_nodemanager_resource_memory___mb: '16384' + YARN_CONF_yarn_nodemanager_resource_cpu___vcores: '8' + YARN_CONF_yarn_nodemanager_disk___health___checker_max___disk___utilization___per___disk___percentage: '98.5' + YARN_CONF_yarn_nodemanager_remote___app___log___dir: /app-logs + YARN_CONF_yarn_nodemanager_aux___services: mapreduce_shuffle + MAPRED_CONF_mapreduce_framework_name: yarn + MAPRED_CONF_mapred_child_java_opts: '-Xmx4096m' + MAPRED_CONF_mapreduce_map_memory_mb: '4096' + MAPRED_CONF_mapreduce_reduce_memory_mb: '8192' + MAPRED_CONF_mapreduce_map_java_opts: '-Xmx3072m' + MAPRED_CONF_mapreduce_reduce_java_opts: '-Xmx6144m' + MAPRED_CONF_yarn_app_mapreduce_am_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ + MAPRED_CONF_mapreduce_map_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ + MAPRED_CONF_mapreduce_reduce_env: HADOOP_MAPRED_HOME=/opt/hadoop-3.2.1/ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: namenode + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: namenode +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 8 }} + spec: + {{ if .Values.persistence.enabled }} + initContainers: + - name: delete-lost-found + image: busybox + command: ["sh", "-c", "rm -rf /hadoop/dfs/name/lost+found"] + volumeMounts: + - name: dfs + mountPath: /hadoop/dfs/name + {{ end }} + containers: + - name: namenode + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.hdfs.namenode.image | default "hadoop-namenode" }}:{{ .Values.modules.hdfs.namenode.imageTag | default "2.0.0-hadoop2.7.4-java8"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CLUSTER_NAME + value: fate + envFrom: + - configMapRef: + name: namenode-env + ports: + - containerPort: 9000 + - containerPort: 9870 + - containerPort: 50070 + volumeMounts: + - mountPath: /etc/hadoop/core-site.xml + subPath: core-site.xml + name: namenode-confs + - name: dfs + mountPath: /hadoop/dfs/name + {{- with .Values.modules.hdfs.namenode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.hdfs.namenode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.hdfs.namenode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: namenode-confs + configMap: + name: namenode-config + - name: dfs + {{ if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.modules.hdfs.namenode.existingClaim | default "namenode-data" }} + {{ else }} + emptyDir: {} + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: namenode + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-9000" + port: 9000 + targetPort: 9000 + protocol: TCP + {{- if eq .Values.modules.hdfs.namenode.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.hdfs.namenode.nodePort }} + {{- end }} + - name: "tcp-9870" + port: 9870 + targetPort: 9870 + protocol: TCP + - name: "tcp-50070" + port: 50070 + targetPort: 50070 + protocol: TCP + type: {{ .Values.modules.hdfs.namenode.type }} + selector: + fateMoudle: namenode +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ if and .Values.persistence.enabled (not .Values.modules.hdfs.namenode.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: namenode-data + labels: + fateMoudle: namenode +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.hdfs.namenode.accessMode }} + resources: + requests: + storage: {{ .Values.modules.hdfs.namenode.size }} + {{ if .Values.modules.hdfs.namenode.storageClass }} + {{ if eq "-" .Values.modules.hdfs.namenode.storageClass }} + storageClassName: "" + {{ else }} + storageClassName: {{ .Values.modules.hdfs.namenode.storageClass }} + {{ end }} + {{ end }} +{{ end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/ingress.yaml b/helm-charts/charts/fate/templates/ingress.yaml new file mode 100644 index 00000000..5f14601a --- /dev/null +++ b/helm-charts/charts/fate/templates/ingress.yaml @@ -0,0 +1,192 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.istio.enabled }} +{{ else }} +{{ if .Values.modules.fateboard.include }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: fateboard + labels: + fateMoudle: fateboard +{{ include "fate.labels" . | indent 4 }} +{{- if .Values.ingress.fateboard.annotations }} + annotations: +{{ toYaml .Values.ingress.fateboard.annotations | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.ingressClassName }} + rules: + {{- range .Values.ingress.fateboard.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: fateboard + port: + number: 8080 + {{- end }} +{{- if .Values.ingress.fateboard.tls }} + tls: +{{ toYaml .Values.ingress.fateboard.tls | indent 4 }} +{{- end }} +--- +{{ end }} + +{{ if .Values.modules.client.include }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: client + labels: + fateMoudle: client +{{ include "fate.labels" . | indent 4 }} +{{- if .Values.ingress.client.annotations }} + annotations: +{{ toYaml .Values.ingress.client.annotations | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.ingressClassName }} + rules: + {{- range .Values.ingress.client.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: notebook + port: + number: 20000 + {{- end }} +{{- if .Values.ingress.client.tls }} + tls: +{{ toYaml .Values.ingress.client.tls | indent 4 }} +{{- end }} +--- +{{ end }} + + +{{ if .Values.modules.spark.include }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: spark + labels: + fateMoudle: spark +{{ include "fate.labels" . | indent 4 }} +{{- if .Values.ingress.spark.annotations }} + annotations: +{{ toYaml .Values.ingress.spark.annotations | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.ingressClassName }} + rules: + {{- range .Values.ingress.spark.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: spark-master + port: + number: 8080 + {{- end }} +{{- if .Values.ingress.spark.tls }} + tls: +{{ toYaml .Values.ingress.spark.tls | indent 4 }} +{{- end }} +--- +{{ end }} + +{{ if .Values.modules.pulsar.include }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pulsar + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +{{- if .Values.ingress.pulsar.annotations }} + annotations: +{{ toYaml .Values.ingress.pulsar.annotations | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.ingressClassName }} + rules: + {{- range .Values.ingress.pulsar.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: pulsar + port: + number: 8080 + {{- end }} +{{- if .Values.ingress.pulsar.tls }} + tls: +{{ toYaml .Values.ingress.pulsar.tls | indent 4 }} +{{- end }} +--- +{{ end }} + +{{ if not .Values.modules.sitePortalServer.tlsEnabled }} +{{ if .Values.modules.frontend.include }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frontend + labels: + fateMoudle: frontend +{{ include "fate.labels" . | indent 4 }} +{{- if .Values.ingress.frontend.annotations }} + annotations: +{{ toYaml .Values.ingress.frontend.annotations | indent 4 }} +{{- end }} +spec: + ingressClassName: {{ .Values.ingressClassName }} + rules: + {{- range .Values.ingress.frontend.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: frontend + port: + number: 8080 + {{- end }} +{{- if .Values.ingress.frontend.tls }} + tls: +{{ toYaml .Values.ingress.frontend.tls | indent 4 }} +{{- end }} +--- +{{ end }} +{{ end }} + + + +{{ end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/istio.yaml b/helm-charts/charts/fate/templates/istio.yaml new file mode 100644 index 00000000..f0ea3df8 --- /dev/null +++ b/helm-charts/charts/fate/templates/istio.yaml @@ -0,0 +1,94 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.istio.enabled }} +apiVersion: networking.istio.io/v1alpha3 +kind: Gateway +metadata: + name: {{ .Values.partyName }}-gateway + labels: +{{ include "fate.labels" . | indent 4 }} +spec: + selector: + istio: ingressgateway # use istio default controller + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "*" +{{ if .Values.modules.fateboard.include }} +--- +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: fateboard + labels: +{{ include "fate.labels" . | indent 4 }} +spec: + hosts: + - {{ .Values.host.fateboard }} + gateways: + - fate-9999-gateway + http: + - route: + - destination: + host: fateboard + port: + number: 8080 +{{ end }} +{{ if .Values.modules.client.include }} +--- +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: notebook + labels: +{{ include "fate.labels" . | indent 4 }} +spec: + hosts: + - {{ .Values.host.client }} + gateways: + - {{ .Values.partyName }}-gateway + http: + - route: + - destination: + host: notebook + port: + number: 20000 +{{ end }} +{{ if .Values.modules.frontend.include }} +--- +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: frontend + labels: +{{ include "fate.labels" . | indent 4 }} +spec: + hosts: + {{- range .Values.ingress.frontend.hosts }} + - {{ .name }} + {{- end }} + gateways: + - {{ .Values.partyName }}-gateway + http: + - route: + - destination: + host: frontend + port: + number: 8080 +{{- end }} +{{ end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/mysql/mysql-module.yaml b/helm-charts/charts/fate/templates/mysql/mysql-module.yaml new file mode 100644 index 00000000..d29fad8c --- /dev/null +++ b/helm-charts/charts/fate/templates/mysql/mysql-module.yaml @@ -0,0 +1,273 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.mysql.include }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysql-config + labels: + fateMoudle: mysql +{{ include "fate.labels" . | indent 4 }} +data: + {{- if eq .Values.modules.python.backend "spark" }} + {{- else }} + create-eggroll-meta-tables.sql: | + -- create database if not exists, default database is eggroll_meta + CREATE DATABASE IF NOT EXISTS `{{ .Values.modules.mysql.database }}`; + + -- all operation under this database + USE `{{ .Values.modules.mysql.database }}`; + + -- store_locator + CREATE TABLE IF NOT EXISTS `store_locator` ( + `store_locator_id` SERIAL PRIMARY KEY, + `store_type` VARCHAR(255) NOT NULL, + `namespace` VARCHAR(2000) NOT NULL DEFAULT 'DEFAULT', + `name` VARCHAR(2000) NOT NULL, + `path` VARCHAR(2000) NOT NULL DEFAULT '', + `total_partitions` INT UNSIGNED NOT NULL, + `partitioner` VARCHAR(2000) NOT NULL DEFAULT 'BYTESTRING_HASH', + `serdes` VARCHAR(2000) NOT NULL DEFAULT '', + `version` INT UNSIGNED NOT NULL DEFAULT 0, + `status` VARCHAR(255) NOT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE UNIQUE INDEX `idx_u_store_locator_ns_n` ON `store_locator` (`namespace`(120), `name`(640)); + CREATE INDEX `idx_store_locator_st` ON `store_locator` (`store_type`(255)); + CREATE INDEX `idx_store_locator_ns` ON `store_locator` (`namespace`(767)); + CREATE INDEX `idx_store_locator_n` ON `store_locator` (`name`(767)); + CREATE INDEX `idx_store_locator_s` ON `store_locator` (`status`(255)); + CREATE INDEX `idx_store_locator_v` ON `store_locator` (`version`); + + + -- store (option) + CREATE TABLE IF NOT EXISTS `store_option` ( + `store_option_id` SERIAL PRIMARY KEY, + `store_locator_id` BIGINT UNSIGNED NOT NULL, + `name` VARCHAR(255) NOT NULL, + `data` VARCHAR(2000) NOT NULL DEFAULT '', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE INDEX `idx_store_option_si` ON `store_option` (`store_locator_id`); + + + -- store_partition + CREATE TABLE IF NOT EXISTS `store_partition` ( + `store_partition_id` SERIAL PRIMARY KEY, -- self-increment sequence + `store_locator_id` BIGINT UNSIGNED NOT NULL, + `node_id` BIGINT UNSIGNED NOT NULL, + `partition_id` INT UNSIGNED NOT NULL, -- partition id of a store + `status` VARCHAR(255) NOT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE UNIQUE INDEX `idx_u_store_partition_si_spi_ni` ON `store_partition` (`store_locator_id`, `store_partition_id`, `node_id`); + CREATE INDEX `idx_store_partition_sli` ON `store_partition` (`store_locator_id`); + CREATE INDEX `idx_store_partition_ni` ON `store_partition` (`node_id`); + CREATE INDEX `idx_store_partition_s` ON `store_partition` (`status`(255)); + + + -- node + CREATE TABLE IF NOT EXISTS `server_node` ( + `server_node_id` SERIAL PRIMARY KEY, + `name` VARCHAR(2000) NOT NULL DEFAULT '', + `server_cluster_id` BIGINT UNSIGNED NOT NULL DEFAULT 0, + `host` VARCHAR(1000) NOT NULL, + `port` INT NOT NULL, + `node_type` VARCHAR(255) NOT NULL, + `status` VARCHAR(255) NOT NULL, + `last_heartbeat_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE INDEX `idx_server_node_h_p_nt` ON `server_node` (`host`(600), `port`, `node_type`(100)); + CREATE INDEX `idx_server_node_h` ON `server_node` (`host`(767)); + CREATE INDEX `idx_server_node_sci` ON `server_node` (`server_cluster_id`); + CREATE INDEX `idx_server_node_nt` ON `server_node` (`node_type`(255)); + CREATE INDEX `idx_server_node_s` ON `server_node` (`status`(255)); + + + -- session (main) + CREATE TABLE IF NOT EXISTS `session_main` ( + `session_id` VARCHAR(767) PRIMARY KEY, + `name` VARCHAR(2000) NOT NULL DEFAULT '', + `status` VARCHAR(255) NOT NULL, + `tag` VARCHAR(255), + `total_proc_count` INT, + `active_proc_count` INT, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE INDEX `idx_session_main_s` ON `session_main` (`status`); + + + -- session (option) + CREATE TABLE IF NOT EXISTS `session_option` ( + `session_option_id` SERIAL PRIMARY KEY, + `session_id` VARCHAR(2000), + `name` VARCHAR(255) NOT NULL, + `data` VARCHAR(2000) NOT NULL DEFAULT '', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE INDEX `idx_session_option_si` ON `session_option` (`session_id`(767)); + + + -- session (processor) + CREATE TABLE IF NOT EXISTS `session_processor` ( + `processor_id` SERIAL PRIMARY KEY, + `session_id` VARCHAR(767), + `server_node_id` INT NOT NULL, + `processor_type` VARCHAR(255) NOT NULL, + `status` VARCHAR(255), + `tag` VARCHAR(255), + `command_endpoint` VARCHAR(255), + `transfer_endpoint` VARCHAR(255), + `pid` INT NOT NULL DEFAULT -1, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci; + + CREATE INDEX `idx_session_processor_si` ON `session_processor` (`session_id`(767)); + + INSERT INTO server_node (host, port, node_type, status) values ('clustermanager', '9460', 'CLUSTER_MANAGER', 'HEALTHY'); + + show tables; + select * from server_node; + {{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + labels: + fateMoudle: mysql +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: mysql +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: mysql +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.mysql.image | default "mysql" }}:{{ .Values.modules.mysql.imageTag | default "8" }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: mysql + env: + - name: MYSQL_ALLOW_EMPTY_PASSWORD + value: "1" + - name: MYSQL_DATABASE + value: {{ .Values.modules.mysql.database | quote }} + - name: MYSQL_USER + value: {{ .Values.modules.mysql.user | quote }} + - name: MYSQL_PASSWORD + value: {{ .Values.modules.mysql.password | quote }} + - name: user + value: root + ports: + - containerPort: 3306 + volumeMounts: + - name: mysql-confs + mountPath: /docker-entrypoint-initdb.d/ + - name: data + mountPath: /var/lib/mysql + subPath: {{ .Values.modules.mysql.subPath }} + {{- with .Values.modules.mysql.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.mysql.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.mysql.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + volumes: + - name: mysql-confs + configMap: + name: mysql-config + {{- if not .Values.persistence.enabled }} + - name: data + emptyDir: {} + {{- else }} + - name: data + persistentVolumeClaim: + claimName: {{ .Values.modules.mysql.existingClaim | default "mysql-data" }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + labels: + fateMoudle: mysql +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-mysql" + port: 3306 + targetPort: 3306 + protocol: TCP + type: {{ .Values.modules.mysql.type }} + selector: + fateMoudle: mysql +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- if and .Values.persistence.enabled (not .Values.modules.mysql.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: mysql-data + labels: + fateMoudle: mysql +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.mysql.accessMode }} + resources: + requests: + storage: {{ .Values.modules.mysql.size }} + {{- if .Values.modules.mysql.storageClass }} + {{- if eq "-" .Values.modules.mysql.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.modules.mysql.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{ end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/nginx/nginx.yaml b/helm-charts/charts/fate/templates/nginx/nginx.yaml new file mode 100644 index 00000000..fa8bc23b --- /dev/null +++ b/helm-charts/charts/fate/templates/nginx/nginx.yaml @@ -0,0 +1,211 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.nginx.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: nginx-config + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +data: + route_table.yaml: | + default: + proxy: + - host: {{ .Values.modules.nginx.exchange.ip }} + http_port: {{ .Values.modules.nginx.exchange.httpPort }} + grpc_port: {{ .Values.modules.nginx.exchange.grpcPort }} + {{ .Values.partyId }}: + proxy: + - host: nginx + http_port: 9300 + grpc_port: 9310 + fateflow: + - host: fateflow + http_port: 9380 + grpc_port: 9360 +{{- range $key, $val := .Values.modules.nginx.route_table }} + {{ $key }}: +{{ toYaml . | indent 6 }} +{{- end }} + nginx.conf: | + + #user nobody; + worker_processes 2; + + #error_log logs/error.log; + #error_log logs/error.log notice; + error_log /dev/stdout info; + error_log /dev/stderr error; + + #pid logs/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" "$http_host" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + '$upstream_status $upstream_addr ' + '$request_time $upstream_response_time' + ; + + access_log logs/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + underscores_in_headers on; + + #gzip on; + lua_package_path "$prefix/lua/?.lua;;"; + init_worker_by_lua_file 'lua/initialize.lua'; + + upstream http_cluster { + server fateflow:9380; # just an invalid address as a place holder + balancer_by_lua_file 'lua/balancer.lua'; + } + + upstream grpc_cluster { + server fateflow:9360; # just an invalid address as a place holder + balancer_by_lua_file 'lua/balancer.lua'; + } + + include vhost/*.conf; + } + + stream { + log_format tcp_proxy '$remote_addr [$time_local] ' + '$protocol $status $bytes_sent $bytes_received ' + '$session_time "$upstream_addr" ' + '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"'; + + access_log logs/tcp-access.log tcp_proxy; + + server { + listen 9128; + proxy_connect_timeout 1s; + proxy_timeout 3s; + proxy_pass 127.0.0.1:3128; + } + } + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: nginx +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: nginx + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.nginx.image | default "nginx" }}:{{ .Values.modules.nginx.imageTag | default "8"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + - | + sed -i "s#conf/route_table.yaml#conf/modifiable/route_table.yaml#g" nginx/lua/route_table.lua; + echo "change path of route_table.yaml success!" + openresty -g 'daemon off;' + ports: + - containerPort: 9300 + - containerPort: 9310 + volumeMounts: + - mountPath: /data/projects/fate/proxy/nginx/conf/nginx.conf + name: nginx-confs + subPath: nginx.conf + - mountPath: /data/projects/fate/proxy/nginx/conf/modifiable/ + name: nginx-confs + {{- with .Values.modules.nginx.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.nginx.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.nginx.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: nginx-confs + configMap: + name: nginx-config +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx + labels: + fateMoudle: nginx +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "http" + port: 9300 + targetPort: 9300 + {{- if eq .Values.modules.nginx.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.nginx.httpNodePort }} + {{- end }} + protocol: TCP + - name: "grpc" + port: 9310 + targetPort: 9310 + {{- if eq .Values.modules.nginx.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.nginx.grpcNodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.nginx.type }} + + {{- if .Values.modules.nginx.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.nginx.loadBalancerIP }}" + {{- end }} + + selector: + fateMoudle: nginx +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} diff --git a/helm-charts/charts/fate/templates/notebook/client.yaml b/helm-charts/charts/fate/templates/notebook/client.yaml new file mode 100644 index 00000000..62d8f2de --- /dev/null +++ b/helm-charts/charts/fate/templates/notebook/client.yaml @@ -0,0 +1,133 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.client.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client + labels: + fateMoudle: client +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: client +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: client +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.client.image | default "client" }}:{{ .Values.modules.client.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: client + command: + - /bin/bash + - -c + - | + flow init --ip ${FATE_FLOW_HOST} --port ${FATE_FLOW_PORT}; + pipeline init --ip ${FATE_FLOW_HOST} --port ${FATE_FLOW_PORT}; + jupyter notebook --ip=0.0.0.0 --port=20000 \ + --allow-root --debug --NotebookApp.notebook_dir='/Examples' \ + --no-browser --NotebookApp.token='' --NotebookApp.password='' + env: + - name: partyId + value: {{ .Values.partyId | quote }} + - name: FATE_FLOW_HOST + value: "fateflow" + - name: FATE_FLOW_PORT + value: "9380" + - name: FATE_SERVING_HOST + value: "{{.Values.modules.serving.ip}}:{{.Values.modules.serving.port}}" + ports: + - containerPort: 20000 + volumeMounts: + - mountPath: /Examples/persistence/ + name: persistence + subPath: {{ .Values.modules.client.subPath }} + {{- with .Values.modules.client.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.client.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.client.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + {{- if not .Values.persistence.enabled }} + - name: persistence + emptyDir: {} + {{- else }} + - name: persistence + persistentVolumeClaim: + claimName: {{ .Values.modules.client.existingClaim | default "client-data" }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: notebook + labels: + fateMoudle: client +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-notebook" + port: 20000 + targetPort: 20000 + protocol: TCP + type: {{ .Values.modules.client.type }} + selector: + fateMoudle: client +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- if and .Values.persistence.enabled (not .Values.modules.client.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: client-data + labels: + fateMoudle: client +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.client.accessMode }} + resources: + requests: + storage: {{ .Values.modules.client.size }} + {{- if .Values.modules.client.storageClass }} + {{- if eq "-" .Values.modules.client.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.modules.client.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/psp.yaml b/helm-charts/charts/fate/templates/psp.yaml new file mode 100644 index 00000000..447017ae --- /dev/null +++ b/helm-charts/charts/fate/templates/psp.yaml @@ -0,0 +1,35 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + labels: + fateMoudle: psp +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/pulsar/pulsar.yaml b/helm-charts/charts/fate/templates/pulsar/pulsar.yaml new file mode 100644 index 00000000..5cbdf969 --- /dev/null +++ b/helm-charts/charts/fate/templates/pulsar/pulsar.yaml @@ -0,0 +1,1109 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.pulsar.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: pulsar-config + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +data: + standalone.conf: | + # + # Licensed to the Apache Software Foundation (ASF) under one + # or more contributor license agreements. See the NOTICE file + # distributed with this work for additional information + # regarding copyright ownership. The ASF licenses this file + # to you under the Apache License, Version 2.0 (the + # "License"); you may not use this file except in compliance + # with the License. You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, + # software distributed under the License is distributed on an + # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + # KIND, either express or implied. See the License for the + # specific language governing permissions and limitations + # under the License. + # + + ### --- General broker settings --- ### + + {{- if .Values.modules.pulsar.exchange }} + brokerClientTlsEnabled=true + {{- else }} + {{- end }} + + # Message Size + maxMessageSize=134217728 + + # Zookeeper quorum connection string + zookeeperServers= + + # Configuration Store connection string + configurationStoreServers= + + {{- if .Values.modules.pulsar.exchange }} + brokerServicePortTls=6651 + {{- else }} + brokerServicePort=6650 + {{- end }} + + # Port to use to server HTTP request + {{- if .Values.modules.pulsar.exchange }} + webServicePortTls=8081 + {{- else }} + webServicePort=8080 + {{- end }} + + # Hostname or IP address the service binds on, default is 0.0.0.0. + bindAddress=0.0.0.0 + + # Hostname or IP address the service advertises to the outside world. If not set, the value of InetAddress.getLocalHost().getHostName() is used. + advertisedAddress= + + # Number of threads to use for Netty IO. Default is set to 2 * Runtime.getRuntime().availableProcessors() + numIOThreads= + + # Number of threads to use for ordered executor. The ordered executor is used to operate with zookeeper, + # such as init zookeeper client, get namespace policies from zookeeper etc. It also used to split bundle. Default is 8 + numOrderedExecutorThreads=8 + + # Number of threads to use for HTTP requests processing. Default is set to 2 * Runtime.getRuntime().availableProcessors() + numHttpServerThreads= + + # Number of thread pool size to use for pulsar broker service. + # The executor in thread pool will do basic broker operation like load/unload bundle, update managedLedgerConfig, + # update topic/subscription/replicator message dispatch rate, do leader election etc. + # Default is Runtime.getRuntime().availableProcessors() + numExecutorThreadPoolSize= + + # Number of thread pool size to use for pulsar zookeeper callback service + # The cache executor thread pool is used for restarting global zookeeper session. + # Default is 10 + numCacheExecutorThreadPoolSize=10 + + # Max concurrent web requests + maxConcurrentHttpRequests=1024 + + # Name of the cluster to which this broker belongs to + clusterName=standalone + + # Enable cluster's failure-domain which can distribute brokers into logical region + failureDomainsEnabled=false + + # Zookeeper session timeout in milliseconds + zooKeeperSessionTimeoutMillis=30000 + + # ZooKeeper operation timeout in seconds + zooKeeperOperationTimeoutSeconds=30 + + # ZooKeeper cache expiry time in seconds + zooKeeperCacheExpirySeconds=300 + + # Time to wait for broker graceful shutdown. After this time elapses, the process will be killed + brokerShutdownTimeoutMs=60000 + + # Flag to skip broker shutdown when broker handles Out of memory error + skipBrokerShutdownOnOOM=false + + # Enable backlog quota check. Enforces action on topic when the quota is reached + backlogQuotaCheckEnabled=true + + # How often to check for topics that have reached the quota + backlogQuotaCheckIntervalInSeconds=60 + + # Default per-topic backlog quota limit + backlogQuotaDefaultLimitGB=10 + + # Default ttl for namespaces if ttl is not already configured at namespace policies. (disable default-ttl with value 0) + ttlDurationDefaultInSeconds=0 + + # Enable the deletion of inactive topics + brokerDeleteInactiveTopicsEnabled=true + + # How often to check for inactive topics + brokerDeleteInactiveTopicsFrequencySeconds=60 + + # Max pending publish requests per connection to avoid keeping large number of pending + # requests in memory. Default: 1000 + maxPendingPublishdRequestsPerConnection=1000 + + # How frequently to proactively check and purge expired messages + messageExpiryCheckIntervalInMinutes=5 + + # How long to delay rewinding cursor and dispatching messages when active consumer is changed + activeConsumerFailoverDelayTimeMillis=1000 + + # How long to delete inactive subscriptions from last consuming + # When it is 0, inactive subscriptions are not deleted automatically + subscriptionExpirationTimeMinutes=0 + + # Enable subscription message redelivery tracker to send redelivery count to consumer (default is enabled) + subscriptionRedeliveryTrackerEnabled=true + + # On KeyShared subscriptions, with default AUTO_SPLIT mode, use splitting ranges or + # consistent hashing to reassign keys to new consumers + subscriptionKeySharedUseConsistentHashing=false + + # On KeyShared subscriptions, number of points in the consistent-hashing ring. + # The higher the number, the more equal the assignment of keys to consumers + subscriptionKeySharedConsistentHashingReplicaPoints=100 + + # How frequently to proactively check and purge expired subscription + subscriptionExpiryCheckIntervalInMinutes=5 + + # Set the default behavior for message deduplication in the broker + # This can be overridden per-namespace. If enabled, broker will reject + # messages that were already stored in the topic + brokerDeduplicationEnabled=false + + # Maximum number of producer information that it's going to be + # persisted for deduplication purposes + brokerDeduplicationMaxNumberOfProducers=10000 + + # Number of entries after which a dedup info snapshot is taken. + # A bigger interval will lead to less snapshots being taken though it would + # increase the topic recovery time, when the entries published after the + # snapshot need to be replayed + brokerDeduplicationEntriesInterval=1000 + + # Time of inactivity after which the broker will discard the deduplication information + # relative to a disconnected producer. Default is 6 hours. + brokerDeduplicationProducerInactivityTimeoutMinutes=360 + + # When a namespace is created without specifying the number of bundle, this + # value will be used as the default + defaultNumberOfNamespaceBundles=4 + + # Enable check for minimum allowed client library version + clientLibraryVersionCheckEnabled=false + + # Path for the file used to determine the rotation status for the broker when responding + # to service discovery health checks + statusFilePath=/usr/local/apache/htdocs + + # Max number of unacknowledged messages allowed to receive messages by a consumer on a shared subscription. Broker will stop sending + # messages to consumer once, this limit reaches until consumer starts acknowledging messages back + # Using a value of 0, is disabling unackeMessage limit check and consumer can receive messages without any restriction + maxUnackedMessagesPerConsumer=50000 + + # Max number of unacknowledged messages allowed per shared subscription. Broker will stop dispatching messages to + # all consumers of the subscription once this limit reaches until consumer starts acknowledging messages back and + # unack count reaches to limit/2. Using a value of 0, is disabling unackedMessage-limit + # check and dispatcher can dispatch messages without any restriction + maxUnackedMessagesPerSubscription=200000 + + # Max number of unacknowledged messages allowed per broker. Once this limit reaches, broker will stop dispatching + # messages to all shared subscription which has higher number of unack messages until subscriptions start + # acknowledging messages back and unack count reaches to limit/2. Using a value of 0, is disabling + # unackedMessage-limit check and broker doesn't block dispatchers + maxUnackedMessagesPerBroker=0 + + # Once broker reaches maxUnackedMessagesPerBroker limit, it blocks subscriptions which has higher unacked messages + # than this percentage limit and subscription will not receive any new messages until that subscription acks back + # limit/2 messages + maxUnackedMessagesPerSubscriptionOnBrokerBlocked=0.16 + + # Tick time to schedule task that checks topic publish rate limiting across all topics + # Reducing to lower value can give more accuracy while throttling publish but + # it uses more CPU to perform frequent check. (Disable publish throttling with value 0) + topicPublisherThrottlingTickTimeMillis=2 + + # Tick time to schedule task that checks broker publish rate limiting across all topics + # Reducing to lower value can give more accuracy while throttling publish but + # it uses more CPU to perform frequent check. (Disable publish throttling with value 0) + brokerPublisherThrottlingTickTimeMillis=50 + + # Max Rate(in 1 seconds) of Message allowed to publish for a broker if broker publish rate limiting enabled + # (Disable message rate limit with value 0) + brokerPublisherThrottlingMaxMessageRate=0 + + # Max Rate(in 1 seconds) of Byte allowed to publish for a broker if broker publish rate limiting enabled + # (Disable byte rate limit with value 0) + brokerPublisherThrottlingMaxByteRate=0 + + # Default messages per second dispatch throttling-limit for every topic. Using a value of 0, is disabling default + # message dispatch-throttling + dispatchThrottlingRatePerTopicInMsg=0 + + # Default bytes per second dispatch throttling-limit for every topic. Using a value of 0, is disabling + # default message-byte dispatch-throttling + dispatchThrottlingRatePerTopicInByte=0 + + # Dispatch rate-limiting relative to publish rate. + # (Enabling flag will make broker to dynamically update dispatch-rate relatively to publish-rate: + # throttle-dispatch-rate = (publish-rate + configured dispatch-rate). + dispatchThrottlingRateRelativeToPublishRate=false + + # By default we enable dispatch-throttling for both caught up consumers as well as consumers who have + # backlog. + dispatchThrottlingOnNonBacklogConsumerEnabled=true + + # Precise dispathcer flow control according to history message number of each entry + preciseDispatcherFlowControl=false + + # Max number of concurrent lookup request broker allows to throttle heavy incoming lookup traffic + maxConcurrentLookupRequest=50000 + + # Max number of concurrent topic loading request broker allows to control number of zk-operations + maxConcurrentTopicLoadRequest=5000 + + # Max concurrent non-persistent message can be processed per connection + maxConcurrentNonPersistentMessagePerConnection=1000 + + # Number of worker threads to serve non-persistent topic + numWorkerThreadsForNonPersistentTopic=8 + + # Enable broker to load persistent topics + enablePersistentTopics=true + + # Enable broker to load non-persistent topics + enableNonPersistentTopics=true + + # Max number of producers allowed to connect to topic. Once this limit reaches, Broker will reject new producers + # until the number of connected producers decrease. + # Using a value of 0, is disabling maxProducersPerTopic-limit check. + maxProducersPerTopic=0 + + # Enforce producer to publish encrypted messages.(default disable). + encryptionRequireOnProducer=false + + # Max number of consumers allowed to connect to topic. Once this limit reaches, Broker will reject new consumers + # until the number of connected consumers decrease. + # Using a value of 0, is disabling maxConsumersPerTopic-limit check. + maxConsumersPerTopic=0 + + # Max number of subscriptions allowed to subscribe to topic. Once this limit reaches, broker will reject + # new subscription until the number of subscribed subscriptions decrease. + # Using a value of 0, is disabling maxSubscriptionsPerTopic limit check. + maxSubscriptionsPerTopic=0 + + # Max number of consumers allowed to connect to subscription. Once this limit reaches, Broker will reject new consumers + # until the number of connected consumers decrease. + # Using a value of 0, is disabling maxConsumersPerSubscription-limit check. + maxConsumersPerSubscription=0 + + # Max number of partitions per partitioned topic + # Use 0 or negative number to disable the check + maxNumPartitionsPerPartitionedTopic=0 + + ### --- TLS --- ### + # Deprecated - Use webServicePortTls and brokerServicePortTls instead + {{- if .Values.modules.pulsar.exchange }} + tlsEnabled=true + {{- else }} + tlsEnabled=false + {{- end }} + + # Tls cert refresh duration in seconds (set 0 to check on every new connection) + tlsCertRefreshCheckDurationSec=300 + + # Path for the TLS certificate file + tlsCertificateFilePath=/opt/pulsar/certs/broker.cert.pem + + # Path for the TLS private key file + tlsKeyFilePath=/opt/pulsar/certs/broker.key-pk8.pem + + # Path for the trusted TLS certificate file. + # This cert is used to verify that any certs presented by connecting clients + # are signed by a certificate authority. If this verification + # fails, then the certs are untrusted and the connections are dropped. + tlsTrustCertsFilePath=/opt/pulsar/certs/ca.cert.pem + + # Accept untrusted TLS certificate from client. + # If true, a client with a cert which cannot be verified with the + # 'tlsTrustCertsFilePath' cert will allowed to connect to the server, + # though the cert will not be used for client authentication. + {{- if .Values.modules.pulsar.exchange }} + tlsAllowInsecureConnection=true + {{- else }} + tlsAllowInsecureConnection=false + {{- end }} + + # Specify the tls protocols the broker will use to negotiate during TLS handshake + # (a comma-separated list of protocol names). + # Examples:- [TLSv1.2, TLSv1.1, TLSv1] + tlsProtocols= + + # Specify the tls cipher the broker will use to negotiate during TLS Handshake + # (a comma-separated list of ciphers). + # Examples:- [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] + tlsCiphers= + + # Trusted client certificates are required for to connect TLS + # Reject the Connection if the Client Certificate is not trusted. + # In effect, this requires that all connecting clients perform TLS client + # authentication. + tlsRequireTrustedClientCertOnConnect=false + + ### --- KeyStore TLS config variables --- ### + # Enable TLS with KeyStore type configuration in broker. + tlsEnabledWithKeyStore=false + + # TLS Provider for KeyStore type + tlsProvider= + + # TLS KeyStore type configuration in broker: JKS, PKCS12 + tlsKeyStoreType=JKS + + # TLS KeyStore path in broker + tlsKeyStore= + + # TLS KeyStore password for broker + tlsKeyStorePassword= + + # TLS TrustStore type configuration in broker: JKS, PKCS12 + tlsTrustStoreType=JKS + + # TLS TrustStore path in broker + tlsTrustStore= + + # TLS TrustStore password for broker + tlsTrustStorePassword= + + # Whether internal client use KeyStore type to authenticate with Pulsar brokers + brokerClientTlsEnabledWithKeyStore=false + + # The TLS Provider used by internal client to authenticate with other Pulsar brokers + brokerClientSslProvider= + + # TLS TrustStore type configuration for internal client: JKS, PKCS12 + # used by the internal client to authenticate with Pulsar brokers + brokerClientTlsTrustStoreType=JKS + + # TLS TrustStore path for internal client + # used by the internal client to authenticate with Pulsar brokers + brokerClientTlsTrustStore= + + # TLS TrustStore password for internal client, + # used by the internal client to authenticate with Pulsar brokers + brokerClientTlsTrustStorePassword= + + # Specify the tls cipher the internal client will use to negotiate during TLS Handshake + # (a comma-separated list of ciphers) + # e.g. [TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]. + # used by the internal client to authenticate with Pulsar brokers + brokerClientTlsCiphers= + + # Specify the tls protocols the broker will use to negotiate during TLS handshake + # (a comma-separated list of protocol names). + # e.g. [TLSv1.2, TLSv1.1, TLSv1] + # used by the internal client to authenticate with Pulsar brokers + brokerClientTlsProtocols= + + # Enable or disable system topic + systemTopicEnabled=false + + # Enable or disable topic level policies, topic level policies depends on the system topic + # Please enable the system topic first. + topicLevelPoliciesEnabled=false + + # If a topic remains fenced for this number of seconds, it will be closed forcefully. + # If it is set to 0 or a negative number, the fenced topic will not be closed. + topicFencingTimeoutSeconds=0 + + ### --- Authentication --- ### + # Role names that are treated as "proxy roles". If the broker sees a request with + #role as proxyRoles - it will demand to see a valid original principal. + proxyRoles= + + # If this flag is set then the broker authenticates the original Auth data + # else it just accepts the originalPrincipal and authorizes it (if required). + authenticateOriginalAuthData=false + + # Enable authentication + authenticationEnabled=false + + # Autentication provider name list, which is comma separated list of class names + authenticationProviders= + + # Enforce authorization + authorizationEnabled=false + + # Authorization provider fully qualified class-name + authorizationProvider=org.apache.pulsar.broker.authorization.PulsarAuthorizationProvider + + # Allow wildcard matching in authorization + # (wildcard matching only applicable if wildcard-char: + # * presents at first or last position eg: *.pulsar.service, pulsar.service.*) + authorizationAllowWildcardsMatching=false + + # Role names that are treated as "super-user", meaning they will be able to do all admin + # operations and publish/consume from all topics + superUserRoles= + + # Authentication settings of the broker itself. Used when the broker connects to other brokers, + # either in same or other clusters + brokerClientAuthenticationPlugin= + brokerClientAuthenticationParameters= + + # Supported Athenz provider domain names(comma separated) for authentication + athenzDomainNames= + + # When this parameter is not empty, unauthenticated users perform as anonymousUserRole + anonymousUserRole= + + # The token "claim" that will be interpreted as the authentication "role" or "principal" by AuthenticationProviderToken (defaults to "sub" if blank) + tokenAuthClaim= + + # The token audience "claim" name, e.g. "aud", that will be used to get the audience from token. + # If not set, audience will not be verified. + tokenAudienceClaim= + + # The token audience stands for this broker. The field `tokenAudienceClaim` of a valid token, need contains this. + tokenAudience= + + ### --- BookKeeper Client --- ### + + # Authentication plugin to use when connecting to bookies + bookkeeperClientAuthenticationPlugin= + + # BookKeeper auth plugin implementatation specifics parameters name and values + bookkeeperClientAuthenticationParametersName= + bookkeeperClientAuthenticationParameters= + + # Timeout for BK add / read operations + bookkeeperClientTimeoutInSeconds=30 + + # Speculative reads are initiated if a read request doesn't complete within a certain time + # Using a value of 0, is disabling the speculative reads + bookkeeperClientSpeculativeReadTimeoutInMillis=0 + + # Number of channels per bookie + bookkeeperNumberOfChannelsPerBookie=16 + + # Enable bookies health check. Bookies that have more than the configured number of failure within + # the interval will be quarantined for some time. During this period, new ledgers won't be created + # on these bookies + bookkeeperClientHealthCheckEnabled=true + bookkeeperClientHealthCheckIntervalSeconds=60 + bookkeeperClientHealthCheckErrorThresholdPerInterval=5 + bookkeeperClientHealthCheckQuarantineTimeInSeconds=1800 + + #bookie quarantine ratio to avoid all clients quarantine the high pressure bookie servers at the same time + bookkeeperClientQuarantineRatio=1.0 + + # Enable rack-aware bookie selection policy. BK will chose bookies from different racks when + # forming a new bookie ensemble + # This parameter related to ensemblePlacementPolicy in conf/bookkeeper.conf, if enabled, ensemblePlacementPolicy + # should be set to org.apache.bookkeeper.client.RackawareEnsemblePlacementPolicy + bookkeeperClientRackawarePolicyEnabled=true + + # Enable region-aware bookie selection policy. BK will chose bookies from + # different regions and racks when forming a new bookie ensemble. + # If enabled, the value of bookkeeperClientRackawarePolicyEnabled is ignored + # This parameter related to ensemblePlacementPolicy in conf/bookkeeper.conf, if enabled, ensemblePlacementPolicy + # should be set to org.apache.bookkeeper.client.RegionAwareEnsemblePlacementPolicy + bookkeeperClientRegionawarePolicyEnabled=false + + # Minimum number of racks per write quorum. BK rack-aware bookie selection policy will try to + # get bookies from at least 'bookkeeperClientMinNumRacksPerWriteQuorum' racks for a write quorum. + bookkeeperClientMinNumRacksPerWriteQuorum=1 + + # Enforces rack-aware bookie selection policy to pick bookies from 'bookkeeperClientMinNumRacksPerWriteQuorum' + # racks for a writeQuorum. + # If BK can't find bookie then it would throw BKNotEnoughBookiesException instead of picking random one. + bookkeeperClientEnforceMinNumRacksPerWriteQuorum=false + + # Enable/disable reordering read sequence on reading entries. + bookkeeperClientReorderReadSequenceEnabled=false + + # Enable bookie isolation by specifying a list of bookie groups to choose from. Any bookie + # outside the specified groups will not be used by the broker + bookkeeperClientIsolationGroups= + + # Enable bookie secondary-isolation group if bookkeeperClientIsolationGroups doesn't + # have enough bookie available. + bookkeeperClientSecondaryIsolationGroups= + + # Minimum bookies that should be available as part of bookkeeperClientIsolationGroups + # else broker will include bookkeeperClientSecondaryIsolationGroups bookies in isolated list. + bookkeeperClientMinAvailableBookiesInIsolationGroups= + + # Set the client security provider factory class name. + # Default: org.apache.bookkeeper.tls.TLSContextFactory + bookkeeperTLSProviderFactoryClass=org.apache.bookkeeper.tls.TLSContextFactory + + # Enable tls authentication with bookie + bookkeeperTLSClientAuthentication=false + + # Supported type: PEM, JKS, PKCS12. Default value: PEM + bookkeeperTLSKeyFileType=PEM + + #Supported type: PEM, JKS, PKCS12. Default value: PEM + bookkeeperTLSTrustCertTypes=PEM + + # Path to file containing keystore password, if the client keystore is password protected. + bookkeeperTLSKeyStorePasswordPath= + + # Path to file containing truststore password, if the client truststore is password protected. + bookkeeperTLSTrustStorePasswordPath= + + # Path for the TLS private key file + bookkeeperTLSKeyFilePath= + + # Path for the TLS certificate file + bookkeeperTLSCertificateFilePath= + + # Path for the trusted TLS certificate file + bookkeeperTLSTrustCertsFilePath=/opt/pulsar/certs/ca.cert.pem + + # Enable/disable disk weight based placement. Default is false + bookkeeperDiskWeightBasedPlacementEnabled=false + + # Set the interval to check the need for sending an explicit LAC + # A value of '0' disables sending any explicit LACs. Default is 0. + bookkeeperExplicitLacIntervalInMills=0 + + # Use older Bookkeeper wire protocol with bookie + bookkeeperUseV2WireProtocol=true + + # Expose bookkeeper client managed ledger stats to prometheus. default is false + # bookkeeperClientExposeStatsToPrometheus=false + + ### --- Managed Ledger --- ### + + # Number of bookies to use when creating a ledger + managedLedgerDefaultEnsembleSize=1 + + # Number of copies to store for each message + managedLedgerDefaultWriteQuorum=1 + + # Number of guaranteed copies (acks to wait before write is complete) + managedLedgerDefaultAckQuorum=1 + + # How frequently to flush the cursor positions that were accumulated due to rate limiting. (seconds). + # Default is 60 seconds + managedLedgerCursorPositionFlushSeconds = 60 + + # Default type of checksum to use when writing to BookKeeper. Default is "CRC32C" + # Other possible options are "CRC32", "MAC" or "DUMMY" (no checksum). + managedLedgerDigestType=CRC32C + + # Number of threads to be used for managed ledger tasks dispatching + managedLedgerNumWorkerThreads=4 + + # Number of threads to be used for managed ledger scheduled tasks + managedLedgerNumSchedulerThreads=4 + + # Amount of memory to use for caching data payload in managed ledger. This memory + # is allocated from JVM direct memory and it's shared across all the topics + # running in the same broker. By default, uses 1/5th of available direct memory + managedLedgerCacheSizeMB= + + # Whether we should make a copy of the entry payloads when inserting in cache + managedLedgerCacheCopyEntries=false + + # Threshold to which bring down the cache level when eviction is triggered + managedLedgerCacheEvictionWatermark=0.9 + + # Configure the cache eviction frequency for the managed ledger cache (evictions/sec) + managedLedgerCacheEvictionFrequency=100.0 + + # All entries that have stayed in cache for more than the configured time, will be evicted + managedLedgerCacheEvictionTimeThresholdMillis=1000 + + # Configure the threshold (in number of entries) from where a cursor should be considered 'backlogged' + # and thus should be set as inactive. + managedLedgerCursorBackloggedThreshold=1000 + + # Rate limit the amount of writes generated by consumer acking the messages + managedLedgerDefaultMarkDeleteRateLimit=0.1 + + # Max number of entries to append to a ledger before triggering a rollover + # A ledger rollover is triggered on these conditions + # * Either the max rollover time has been reached + # * or max entries have been written to the ledged and at least min-time + # has passed + managedLedgerMaxEntriesPerLedger=50000 + + # Minimum time between ledger rollover for a topic + managedLedgerMinLedgerRolloverTimeMinutes=10 + + # Maximum time before forcing a ledger rollover for a topic + managedLedgerMaxLedgerRolloverTimeMinutes=240 + + # Max number of entries to append to a cursor ledger + managedLedgerCursorMaxEntriesPerLedger=50000 + + # Max time before triggering a rollover on a cursor ledger + managedLedgerCursorRolloverTimeInSeconds=14400 + + # Maximum ledger size before triggering a rollover for a topic (MB) + managedLedgerMaxSizePerLedgerMbytes=2048 + + # Max number of "acknowledgment holes" that are going to be persistently stored. + # When acknowledging out of order, a consumer will leave holes that are supposed + # to be quickly filled by acking all the messages. The information of which + # messages are acknowledged is persisted by compressing in "ranges" of messages + # that were acknowledged. After the max number of ranges is reached, the information + # will only be tracked in memory and messages will be redelivered in case of + # crashes. + managedLedgerMaxUnackedRangesToPersist=10000 + + # Max number of "acknowledgment holes" that can be stored in Zookeeper. If number of unack message range is higher + # than this limit then broker will persist unacked ranges into bookkeeper to avoid additional data overhead into + # zookeeper. + managedLedgerMaxUnackedRangesToPersistInZooKeeper=1000 + + # Skip reading non-recoverable/unreadable data-ledger under managed-ledger's list. It helps when data-ledgers gets + # corrupted at bookkeeper and managed-cursor is stuck at that ledger. + autoSkipNonRecoverableData=false + + # operation timeout while updating managed-ledger metadata. + managedLedgerMetadataOperationsTimeoutSeconds=60 + + # Read entries timeout when broker tries to read messages from bookkeeper. + managedLedgerReadEntryTimeoutSeconds=0 + + # Add entry timeout when broker tries to publish message to bookkeeper (0 to disable it). + managedLedgerAddEntryTimeoutSeconds=0 + + # New entries check delay for the cursor under the managed ledger. + # If no new messages in the topic, the cursor will try to check again after the delay time. + # For consumption latency sensitive scenario, can set to a smaller value or set to 0. + # Of course, use a smaller value may degrade consumption throughput. Default is 10ms. + managedLedgerNewEntriesCheckDelayInMillis=10 + + # Use Open Range-Set to cache unacked messages + managedLedgerUnackedRangesOpenCacheSetEnabled=true + + # Managed ledger prometheus stats latency rollover seconds (default: 60s) + managedLedgerPrometheusStatsLatencyRolloverSeconds=60 + + # Whether trace managed ledger task execution time + managedLedgerTraceTaskExecution=true + + ### --- Load balancer --- ### + + loadManagerClassName=org.apache.pulsar.broker.loadbalance.NoopLoadManager + + # Enable load balancer + loadBalancerEnabled=false + + # Percentage of change to trigger load report update + loadBalancerReportUpdateThresholdPercentage=10 + + # maximum interval to update load report + loadBalancerReportUpdateMaxIntervalMinutes=15 + + # Frequency of report to collect + loadBalancerHostUsageCheckIntervalMinutes=1 + + # Load shedding interval. Broker periodically checks whether some traffic should be offload from + # some over-loaded broker to other under-loaded brokers + loadBalancerSheddingIntervalMinutes=1 + + # Prevent the same topics to be shed and moved to other broker more that once within this timeframe + loadBalancerSheddingGracePeriodMinutes=30 + + # Usage threshold to allocate max number of topics to broker + loadBalancerBrokerMaxTopics=50000 + + # Interval to flush dynamic resource quota to ZooKeeper + loadBalancerResourceQuotaUpdateIntervalMinutes=15 + + # enable/disable namespace bundle auto split + loadBalancerAutoBundleSplitEnabled=true + + # enable/disable automatic unloading of split bundles + loadBalancerAutoUnloadSplitBundlesEnabled=true + + # maximum topics in a bundle, otherwise bundle split will be triggered + loadBalancerNamespaceBundleMaxTopics=1000 + + # maximum sessions (producers + consumers) in a bundle, otherwise bundle split will be triggered + loadBalancerNamespaceBundleMaxSessions=1000 + + # maximum msgRate (in + out) in a bundle, otherwise bundle split will be triggered + loadBalancerNamespaceBundleMaxMsgRate=30000 + + # maximum bandwidth (in + out) in a bundle, otherwise bundle split will be triggered + loadBalancerNamespaceBundleMaxBandwidthMbytes=100 + + # maximum number of bundles in a namespace + loadBalancerNamespaceMaximumBundles=128 + + # The broker resource usage threshold. + # When the broker resource usage is gratter than the pulsar cluster average resource usge, + # the threshold shedder will be triggered to offload bundles from the broker. + # It only take effect in ThresholdSheddler strategy. + loadBalancerBrokerThresholdShedderPercentage=10 + + # When calculating new resource usage, the history usage accounts for. + # It only take effect in ThresholdSheddler strategy. + loadBalancerHistoryResourcePercentage=0.9 + + # The BandWithIn usage weight when calculating new resourde usage. + # It only take effect in ThresholdShedder strategy. + loadBalancerBandwithInResourceWeight=1.0 + + # The BandWithOut usage weight when calculating new resourde usage. + # It only take effect in ThresholdShedder strategy. + loadBalancerBandwithOutResourceWeight=1.0 + + # The CPU usage weight when calculating new resourde usage. + # It only take effect in ThresholdShedder strategy. + loadBalancerCPUResourceWeight=1.0 + + # The heap memory usage weight when calculating new resourde usage. + # It only take effect in ThresholdShedder strategy. + loadBalancerMemoryResourceWeight=1.0 + + # The direct memory usage weight when calculating new resourde usage. + # It only take effect in ThresholdShedder strategy. + loadBalancerDirectMemoryResourceWeight=1.0 + + # Bundle unload minimum throughput threshold (MB), avoding bundle unload frequently. + # It only take effect in ThresholdShedder strategy. + loadBalancerBundleUnloadMinThroughputThreshold=10 + + ### --- Replication --- ### + + # Enable replication metrics + replicationMetricsEnabled=true + + # Max number of connections to open for each broker in a remote cluster + # More connections host-to-host lead to better throughput over high-latency + # links. + replicationConnectionsPerBroker=16 + + # Replicator producer queue size + replicationProducerQueueSize=1000 + + # Duration to check replication policy to avoid replicator inconsistency + # due to missing ZooKeeper watch (disable with value 0) + replicationPolicyCheckDurationSeconds=600 + + # Default message retention time + defaultRetentionTimeInMinutes=0 + + # Default retention size + defaultRetentionSizeInMB=0 + + # How often to check whether the connections are still alive + keepAliveIntervalSeconds=30 + + ### --- WebSocket --- ### + + # Enable the WebSocket API service in broker + webSocketServiceEnabled=true + + # Number of IO threads in Pulsar Client used in WebSocket proxy + webSocketNumIoThreads=8 + + # Number of connections per Broker in Pulsar Client used in WebSocket proxy + webSocketConnectionsPerBroker=8 + + # Time in milliseconds that idle WebSocket session times out + webSocketSessionIdleTimeoutMillis=300000 + + # The maximum size of a text message during parsing in WebSocket proxy + webSocketMaxTextFrameSize=1048576 + + ### --- Metrics --- ### + + # Enable topic level metrics + exposeTopicLevelMetricsInPrometheus=true + + # Classname of Pluggable JVM GC metrics logger that can log GC specific metrics + # jvmGCMetricsLoggerClassName= + + ### --- Broker Web Stats --- ### + + # Enable topic level metrics + exposePublisherStats=true + + # Enable expose the precise backlog stats. + # Set false to use published counter and consumed counter to calculate, this would be more efficient but may be inaccurate. + # Default is false. + exposePreciseBacklogInPrometheus=false + + ### --- Deprecated config variables --- ### + + # Deprecated. Use configurationStoreServers + globalZookeeperServers= + + # Deprecated. Use brokerDeleteInactiveTopicsFrequencySeconds + brokerServicePurgeInactiveFrequencyInSeconds=60 + + ### --- BookKeeper Configuration --- ##### + + ledgerStorageClass=org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage + + # The maximum netty frame size in bytes. Any message received larger than this will be rejected. The default value is 5MB. + nettyMaxFrameSizeBytes=134217728 + + # Size of Write Cache. Memory is allocated from JVM direct memory. + # Write cache is used to buffer entries before flushing into the entry log + # For good performance, it should be big enough to hold a substantial amount + # of entries in the flush interval + # By default it will be allocated to 1/4th of the available direct memory + dbStorage_writeCacheMaxSizeMb= + + # Size of Read cache. Memory is allocated from JVM direct memory. + # This read cache is pre-filled doing read-ahead whenever a cache miss happens + # By default it will be allocated to 1/4th of the available direct memory + dbStorage_readAheadCacheMaxSizeMb= + + # How many entries to pre-fill in cache after a read cache miss + dbStorage_readAheadCacheBatchSize=1000 + + flushInterval=60000 + + ## RocksDB specific configurations + ## DbLedgerStorage uses RocksDB to store the indexes from + ## (ledgerId, entryId) -> (entryLog, offset) + + # Size of RocksDB block-cache. For best performance, this cache + # should be big enough to hold a significant portion of the index + # database which can reach ~2GB in some cases + # Default is to use 10% of the direct memory size + dbStorage_rocksDB_blockCacheSize= + + # Other RocksDB specific tunables + dbStorage_rocksDB_writeBufferSizeMB=4 + dbStorage_rocksDB_sstSizeInMB=4 + dbStorage_rocksDB_blockSize=4096 + dbStorage_rocksDB_bloomFilterBitsPerKey=10 + dbStorage_rocksDB_numLevels=-1 + dbStorage_rocksDB_numFilesInLevel0=4 + dbStorage_rocksDB_maxSizeInLevel1MB=256 + + # Maximum latency to impose on a journal write to achieve grouping + journalMaxGroupWaitMSec=1 + + # Should the data be fsynced on journal before acknowledgment. + journalSyncData=false + + + # For each ledger dir, maximum disk space which can be used. + # Default is 0.95f. i.e. 95% of disk can be used at most after which nothing will + # be written to that partition. If all ledger dir partions are full, then bookie + # will turn to readonly mode if 'readOnlyModeEnabled=true' is set, else it will + # shutdown. + # Valid values should be in between 0 and 1 (exclusive). + diskUsageThreshold=0.99 + + # The disk free space low water mark threshold. + # Disk is considered full when usage threshold is exceeded. + # Disk returns back to non-full state when usage is below low water mark threshold. + # This prevents it from going back and forth between these states frequently + # when concurrent writes and compaction are happening. This also prevent bookie from + # switching frequently between read-only and read-writes states in the same cases. + diskUsageWarnThreshold=0.99 + + # Whether the bookie allowed to use a loopback interface as its primary + # interface(i.e. the interface it uses to establish its identity)? + # By default, loopback interfaces are not allowed as the primary + # interface. + # Using a loopback interface as the primary interface usually indicates + # a configuration error. For example, its fairly common in some VPS setups + # to not configure a hostname, or to have the hostname resolve to + # 127.0.0.1. If this is the case, then all bookies in the cluster will + # establish their identities as 127.0.0.1:3181, and only one will be able + # to join the cluster. For VPSs configured like this, you should explicitly + # set the listening interface. + allowLoopback=true + + # How long the interval to trigger next garbage collection, in milliseconds + # Since garbage collection is running in background, too frequent gc + # will heart performance. It is better to give a higher number of gc + # interval if there is enough disk capacity. + gcWaitTime=300000 + + # Enable topic auto creation if new producer or consumer connected (disable auto creation with value false) + allowAutoTopicCreation=true + + # The type of topic that is allowed to be automatically created.(partitioned/non-partitioned) + allowAutoTopicCreationType=non-partitioned + + # Enable subscription auto creation if new consumer connected (disable auto creation with value false) + allowAutoSubscriptionCreation=true + + # The number of partitioned topics that is allowed to be automatically created if allowAutoTopicCreationType is partitioned. + defaultNumPartitions=1 + + ### --- Transaction config variables --- ### + transactionMetadataStoreProviderClassName=org.apache.pulsar.transaction.coordinator.impl.InMemTransactionMetadataStoreProvider + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pulsar + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: pulsar +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: pulsar + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.pulsar.image | default "pulsar" }}:{{ .Values.modules.pulsar.imageTag | default "2.7.0"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/bash + - -c + - "bin/pulsar standalone -nss" + env: + ports: + - containerPort: 6650 + - containerPort: 6651 + - containerPort: 8080 + - containerPort: 8081 + volumeMounts: + - mountPath: /pulsar/conf/standalone.conf + name: pulsar-confs + subPath: standalone.conf + - mountPath: /pulsar/data + name: pulsar-data + {{- with .Values.modules.pulsar.exchange }} + - mountPath: /opt/pulsar/certs/ + name: pulsar-cert + {{- end }} + {{- with .Values.modules.pulsar.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.pulsar.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.pulsar.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: pulsar-confs + configMap: + name: pulsar-config + {{- with .Values.modules.pulsar.exchange }} + - name: pulsar-cert + secret: + secretName: pulsar-cert + {{- end }} + - name: pulsar-data + {{ if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.modules.pulsar.existingClaim | default "pulsar-data" }} + {{ else }} + emptyDir: {} + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: pulsar + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "http-port" + port: 6650 + targetPort: 6650 + {{- if eq .Values.modules.pulsar.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.pulsar.httpNodePort }} + {{- end }} + protocol: TCP + - name: "https-port" + port: 6651 + targetPort: 6651 + {{- if eq .Values.modules.pulsar.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.pulsar.httpsNodePort }} + {{- end }} + protocol: TCP + - name: "mng-port" + port: 8080 + targetPort: 8080 + protocol: TCP + - name: "mngs-port" + port: 8081 + targetPort: 8081 + protocol: TCP + type: {{ .Values.modules.pulsar.type }} + + {{- if .Values.modules.pulsar.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.pulsar.loadBalancerIP }}" + {{- end }} + + selector: + fateMoudle: pulsar +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ if .Values.modules.pulsar.publicLB.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: pulsar-public-tls + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tls-port" + port: 6651 + targetPort: 6651 + protocol: TCP + type: LoadBalancer + selector: + fateMoudle: pulsar +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} +{{ if and .Values.persistence.enabled (not .Values.modules.pulsar.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: pulsar-data + labels: + fateMoudle: pulsar +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.pulsar.accessMode }} + resources: + requests: + storage: {{ .Values.modules.pulsar.size }} + {{ if .Values.modules.pulsar.storageClass }} + {{ if eq "-" .Values.modules.pulsar.storageClass }} + storageClassName: "" + {{ else }} + storageClassName: {{ .Values.modules.pulsar.storageClass }} + {{ end }} + {{ end }} +{{ end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/role.yaml b/helm-charts/charts/fate/templates/role.yaml new file mode 100644 index 00000000..d3bd8887 --- /dev/null +++ b/helm-charts/charts/fate/templates/role.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + fateMoudle: role +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +rules: + - apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ .Values.partyName }}] +{{- end }} diff --git a/helm-charts/charts/fate/templates/rolebinding.yaml b/helm-charts/charts/fate/templates/rolebinding.yaml new file mode 100644 index 00000000..df87f27c --- /dev/null +++ b/helm-charts/charts/fate/templates/rolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + fateMoudle: serviceAccount +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Values.partyName }} +subjects: + - kind: ServiceAccount + name: {{ .Values.partyName }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm-charts/charts/fate/templates/serviceaccount.yaml b/helm-charts/charts/fate/templates/serviceaccount.yaml new file mode 100644 index 00000000..4a1f9a58 --- /dev/null +++ b/helm-charts/charts/fate/templates/serviceaccount.yaml @@ -0,0 +1,23 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + fateMoudle: serviceAccount +{{ include "fate.labels" . | indent 4 }} + name: {{ .Values.partyName }} +{{- end }} diff --git a/helm-charts/charts/fate/templates/site-portal/forntend.yaml b/helm-charts/charts/fate/templates/site-portal/forntend.yaml new file mode 100644 index 00000000..504d45a7 --- /dev/null +++ b/helm-charts/charts/fate/templates/site-portal/forntend.yaml @@ -0,0 +1,195 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.frontend.include }} +{{ if .Values.modules.sitePortalServer.tlsEnabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-conf-https +data: + nginx.conf.template: | + worker_processes auto; + pid /tmp/nginx.pid; + + events { + worker_connections 1024; + } + + http { + + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + upstream server { + server ${SITEPORTAL_SERVER_HOST}:8443; + } + + server { + listen 8443 ssl; + server_name localhost; + + ssl_certificate /var/lib/site-portal/cert/server.crt; + ssl_certificate_key /var/lib/site-portal/cert/server.key; + ssl_client_certificate /var/lib/site-portal/cert/ca.crt; + ssl_verify_client optional; + + ssl_protocols TLSv1.2; + ssl_ciphers '!aNULL:kECDH+AESGCM:ECDH+AESGCM:RSA+AESGCM:kECDH+AES:ECDH+AES:RSA+AES:'; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /api/ { + proxy_pass https://server/api/; + + proxy_ssl_certificate /var/lib/site-portal/cert/client.crt; + proxy_ssl_certificate_key /var/lib/site-portal/cert/client.key; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_set_header X-SP-CLIENT-CERT $ssl_client_escaped_cert; + proxy_set_header X-SP-CLIENT-SDN $ssl_client_s_dn; + proxy_set_header X-SP-CLIENT-VERIFY $ssl_client_verify; + } + } + } +--- +{{ end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + fateMoudle: frontend +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: frontend +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: frontend +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: frontend + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.frontend.image | default "site-portal-frontend" }}:{{ .Values.modules.frontend.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + - containerPort: 8443 + {{ else }} + - containerPort: 8080 + {{ end }} + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + volumeMounts: + - mountPath: /var/lib/site-portal/cert + name: site-portal-cert + - mountPath: /etc/nginx/conf.d/nginx.conf.template + name: nginx-conf-https + subPath: nginx.conf.template + {{ end }} + env: + - name: SITEPORTAL_SERVER_HOST + value: site-portal-server + {{- with .Values.modules.frontend.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.frontend.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.frontend.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + volumes: + - name: site-portal-cert + secret: + secretName: site-portal-cert + - name: nginx-conf-https + configMap: + name: nginx-conf-https + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + fateMoudle: frontend +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "http-frontend" + port: 8080 + targetPort: 8080 + {{- if eq .Values.modules.frontend.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.frontend.nodePort }} + {{- end }} + protocol: TCP + - name: "https-frontend" + port: 8443 + targetPort: 8443 + {{- if eq .Values.modules.frontend.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.frontend.nodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.frontend.type }} + {{- if .Values.modules.frontend.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.frontend.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: frontend +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} diff --git a/helm-charts/charts/fate/templates/site-portal/postgres-module.yaml b/helm-charts/charts/fate/templates/site-portal/postgres-module.yaml new file mode 100644 index 00000000..746061b3 --- /dev/null +++ b/helm-charts/charts/fate/templates/site-portal/postgres-module.yaml @@ -0,0 +1,128 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.postgres.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: postgres +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: postgres + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.postgres.image | default "postgres" }}:{{ .Values.modules.postgres.imageTag | default "13.3"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: POSTGRES_USER + value: {{ .Values.modules.postgres.user | quote }} + - name: POSTGRES_PASSWORD + value: {{ .Values.modules.postgres.password | quote }} + - name: POSTGRES_DB + value: {{ .Values.modules.postgres.db | quote }} + ports: + - containerPort: 5432 + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgres-data + subPath: data + {{- with .Values.modules.postgres.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.postgres.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.postgres.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + {{- if not .Values.persistence.enabled }} + - name: postgres-data + emptyDir: {} + {{- else }} + - name: postgres-data + persistentVolumeClaim: + claimName: {{ .Values.modules.postgres.existingClaim | default "postgres-data" }} + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-postgres" + port: 5432 + targetPort: 5432 + {{- if eq .Values.modules.postgres.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.postgres.nodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.postgres.type }} + {{- if .Values.modules.postgres.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.postgres.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: postgres +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{- if and .Values.persistence.enabled (not .Values.modules.postgres.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: postgres-data + labels: + fateMoudle: postgres +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.postgres.accessMode }} + resources: + requests: + storage: {{ .Values.modules.postgres.size }} + {{- if .Values.modules.postgres.storageClass }} + {{- if eq "-" .Values.modules.postgres.storageClass }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.modules.postgres.storageClass }} + {{- end }} + {{- end }} +{{- end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/site-portal/site-portal-server-module.yaml b/helm-charts/charts/fate/templates/site-portal/site-portal-server-module.yaml new file mode 100644 index 00000000..7c4a2b6f --- /dev/null +++ b/helm-charts/charts/fate/templates/site-portal/site-portal-server-module.yaml @@ -0,0 +1,174 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.sitePortalServer.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: site-portal-server + labels: + fateMoudle: site-portal-server +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: site-portal-server +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: site-portal-server +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: site-portal-server + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.sitePortalServer.image | default "site-portal-server" }}:{{ .Values.modules.sitePortalServer.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + - containerPort: {{ .Values.modules.sitePortalServer.tlsPort }} + {{ else }} + - containerPort: 8080 + {{ end }} + env: + - name: POSTGRES_HOST + value: {{ .Values.modules.sitePortalServer.postgresHost | quote }} + - name: POSTGRES_PORT + value: {{ .Values.modules.sitePortalServer.postgresPort | quote }} + - name: POSTGRES_USER + value: {{ .Values.modules.sitePortalServer.postgresUser | quote }} + - name: POSTGRES_DB + value: {{ .Values.modules.sitePortalServer.postgresDb | quote }} + - name: POSTGRES_PASSWORD + value: {{ .Values.modules.sitePortalServer.postgresPassword | quote }} + - name: SITEPORTAL_INITIAL_ADMIN_PASSWORD + value: {{ .Values.modules.sitePortalServer.adminPassword | quote }} + - name: SITEPORTAL_INITIAL_USER_PASSWORD + value: {{ .Values.modules.sitePortalServer.userPassword | quote }} + - name: SITEPORTAL_LOCALDATA_BASEDIR + value: /var/lib/site-portal/data/uploaded + - name: SITEPORTAL_TLS_ENABLED + value: {{ .Values.modules.sitePortalServer.tlsEnabled | quote }} + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + - name: SITEPORTAL_TLS_SERVER_CERT + value: {{ .Values.modules.sitePortalServer.serverCert | quote }} + - name: SITEPORTAL_TLS_SERVER_KEY + value: {{ .Values.modules.sitePortalServer.serverKey | quote }} + - name: SITEPORTAL_TLS_CLIENT_CERT + value: {{ .Values.modules.sitePortalServer.clientCert | quote }} + - name: SITEPORTAL_TLS_CLIENT_KEY + value: {{ .Values.modules.sitePortalServer.clientKey | quote }} + - name: SITEPORTAL_TLS_CA_CERT + value: {{ .Values.modules.sitePortalServer.caCert | quote }} + - name: SITEPORTAL_TLS_PORT + value: {{ .Values.modules.sitePortalServer.tlsPort | quote }} + - name: SITEPORTAL_TLS_COMMON_NAME + value: {{ .Values.modules.sitePortalServer.tlsCommonName | quote }} + {{ end }} + volumeMounts: + - mountPath: /var/lib/site-portal/data/uploaded + name: uploaded + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + - mountPath: /var/lib/site-portal/cert + name: site-portal-cert + {{ end }} + {{- with .Values.modules.sitePortalServer.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.sitePortalServer.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.sitePortalServer.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: uploaded + {{ if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.modules.sitePortalServer.existingClaim | default "site-portal-server-uploaded-data" }} + {{ else }} + emptyDir: {} + {{ end }} + {{ if .Values.modules.sitePortalServer.tlsEnabled }} + - name: site-portal-cert + secret: + secretName: site-portal-cert + {{ end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: site-portal-server + labels: + fateMoudle: site-portal-server +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-site-portal-server" + port: 8080 + targetPort: 8080 + {{- if eq .Values.modules.sitePortalServer.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.sitePortalServer.nodePort }} + {{- end }} + protocol: TCP + - name: "https-site-portal-server" + port: 8443 + targetPort: {{ .Values.modules.sitePortalServer.tlsPort }} + {{- if eq .Values.modules.sitePortalServer.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.sitePortalServer.nodePort }} + {{- end }} + protocol: TCP + type: {{ .Values.modules.sitePortalServer.type }} + {{- if .Values.modules.sitePortalServer.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.sitePortalServer.loadBalancerIP }}" + {{- end }} + selector: + fateMoudle: site-portal-server +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ if and .Values.persistence.enabled (not .Values.modules.sitePortalServer.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: site-portal-server-uploaded-data + labels: + fateMoudle: sitePortalServer +{{ include "fate.labels" . | indent 4 }} +spec: + accessModes: + - {{ .Values.modules.sitePortalServer.accessMode }} + resources: + requests: + storage: {{ .Values.modules.sitePortalServer.size }} + {{ if .Values.modules.sitePortalServer.storageClass }} + {{ if eq "-" .Values.modules.sitePortalServer.storageClass }} + storageClassName: "" + {{ else }} + storageClassName: {{ .Values.modules.sitePortalServer.storageClass }} + {{ end }} + {{ end }} +{{ end }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/spark/spark-master.yaml b/helm-charts/charts/fate/templates/spark/spark-master.yaml new file mode 100644 index 00000000..982c87f6 --- /dev/null +++ b/helm-charts/charts/fate/templates/spark/spark-master.yaml @@ -0,0 +1,127 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.spark.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spark-master + labels: + fateMoudle: spark-master +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: {{ default 1 .Values.modules.spark.master.replicas }} + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: spark-master +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: spark-master +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: spark-master + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.spark.master.image | default "spark-master" }}:{{ .Values.modules.spark.master.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.modules.spark.master.resources}} + resources: + {{- range $key, $val := .Values.modules.spark.master.resources }} + {{ $key }}: +{{ toYaml $val | indent 14 }} + {{- end }} + {{- end }} + ports: + - containerPort: 8080 + - containerPort: 7077 + - containerPort: 6066 + {{- with .Values.modules.spark.master.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.spark.master.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.spark.master.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: spark-master + labels: + fateMoudle: spark-master +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: web-ui + protocol: TCP + port: 8080 + targetPort: 8080 + - name: master + protocol: TCP + port: 7077 + targetPort: 7077 + - name: master-rest + protocol: TCP + port: 6066 + targetPort: 6066 + type: ClusterIP + clusterIP: None + selector: + fateMoudle: spark-master +{{ include "fate.matchLabels" . | indent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: spark-client + labels: + fateMoudle: spark-master +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: web-ui + protocol: TCP + port: 8080 + targetPort: 8080 + - name: master + protocol: TCP + port: 7077 + targetPort: 7077 + {{- if eq .Values.modules.spark.master.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.spark.master.nodePort }} + {{- end }} + - name: master-rest + protocol: TCP + port: 6066 + targetPort: 6066 + type: {{ .Values.modules.spark.master.type }} + # clusterIP: None + selector: + fateMoudle: spark-master +{{ include "fate.matchLabels" . | indent 4 }} +{{ end }} diff --git a/helm-charts/charts/fate/templates/spark/spark-worker.yaml b/helm-charts/charts/fate/templates/spark/spark-worker.yaml new file mode 100644 index 00000000..886c29f1 --- /dev/null +++ b/helm-charts/charts/fate/templates/spark/spark-worker.yaml @@ -0,0 +1,114 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{ if .Values.modules.spark.include }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: spark-worker-config + labels: + fateMoudle: spark-worker +{{ include "fate.labels" . | indent 4 }} +data: + service_conf.yaml: | + work_mode: 1 + database: + name: fate_flow + user: fate + passwd: fate + host: 127.0.0.1 + port: 3306 + max_connections: 100 + stale_timeout: 30 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spark-worker + labels: + fateMoudle: spark-worker +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: {{ default 2 .Values.modules.spark.worker.replicas }} + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: spark-worker +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + labels: + fateMoudle: spark-worker +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + - name: spark-worker + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.spark.worker.image | default "spark-worker" }}:{{ .Values.modules.spark.worker.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.modules.spark.worker.resources }} + resources: + {{- range $key, $val := .Values.modules.spark.worker.resources }} + {{ $key }}: +{{ toYaml $val | indent 14 }} + {{- end }} + {{- end }} + volumeMounts: + - mountPath: /data/projects/fate/conf/ + name: spark-worker-confs + ports: + - containerPort: 8081 + {{- with .Values.modules.spark.worker.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.spark.worker.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.spark.worker.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: spark-worker-confs + configMap: + name: spark-worker-config +--- +apiVersion: v1 +kind: Service +metadata: + name: spark-worker-1 + labels: + fateMoudle: spark-worker +{{ include "fate.labels" . | indent 4 }} +spec: + ports: + - name: "tcp-spark" + port: 8081 + targetPort: 8081 + protocol: TCP + type: {{ .Values.modules.spark.worker.type }} + clusterIP: None + selector: + fateMoudle: spark-worker +{{ include "fate.matchLabels" . | indent 4 }} +--- +{{ end }} diff --git a/helm-charts/charts/fate/values-template-example.yaml b/helm-charts/charts/fate/values-template-example.yaml new file mode 100644 index 00000000..2253ef17 --- /dev/null +++ b/helm-charts/charts/fate/values-template-example.yaml @@ -0,0 +1,311 @@ +name: site-portal-9999 +namespace: site-portal-9999 +chartName: fate +chartVersion: v1.6.1-fedlcm-v0.1.0 +partyId: 9999 +registry: "" +pullPolicy: IfNotPresent +imagePullSecrets: +- name: myregistrykey +persistence: false +istio: + enabled: false +podSecurityPolicy: + enabled: false +modules: + - mysql + - python + - fateboard + - client + - frontend + - sitePortalServer + - postgres + - spark + - hdfs + - nginx + - pulsar + +# ingress: + # fateboard: + # annotations: + # hosts: + # - name: party9999.fateboard.example.com + # path: / + # tls: + # - secretName: my-tls-secret + # hosts: + # - party9999.fateboard.example.com + # client: + # hosts: + # - name: party9999.notebook.example.com + # spark: + # hosts: + # - name: party9999.spark.example.com + # frontend: + # annotations: + # hosts: + # - name: party9999.frontend.example.com + +# python: + # image: federatedai/python-spark + # imageTag: 1.6.1-release + # type: NodePort + # httpNodePort: 30097 + # grpcNodePort: 30092 + # loadBalancerIP: + # serviceAccountName: "" + # resources: + # nodeSelector: + # tolerations: + # affinity: + # enabledNN: false + # existingClaim: "" + # storageClass: "python" + # accessMode: ReadWriteMany + # size: 1Gi + # clustermanager: + # cores_per_node: 16 + # nodes: 2 + # spark: + # cores_per_node: 20 + # nodes: 2 + # master: spark://spark-master:7077 + # driverHost: + # driverHostType: + # portMaxRetries: + # driverStartPort: + # blockManagerStartPort: + # pysparkPython: + # hdfs: + # name_node: hdfs://namenode:9000 + # path_prefix: + # rabbitmq: + # host: rabbitmq + # mng_port: 15672 + # port: 5672 + # user: fate + # password: fate + # pulsar: + # host: pulsar + # mng_port: 8080 + # port: 6650 + # nginx: + # host: nginx + # http_port: 9300 + # grpc_port: 9310 + +# fateboard: + # image: federatedai/fateboard + # imageTag: 1.6.1-release + # type: ClusterIP + # username: admin + # password: admin + +# client: + # image: federatedai/client + # imageTag: 1.6.1-release + # nodeSelector: + # subPath: "" + # existingClaim: "" + # storageClass: "client" + # accessMode: ReadWriteOnce + # size: 1Gi + +# mysql: + # image: mysql + # imageTag: 8 + # nodeSelector: + # tolerations: + # affinity: + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + # subPath: "" + # existingClaim: "" + # storageClass: "mysql" + # accessMode: ReadWriteOnce + # size: 1Gi + + +# externalMysqlIp: mysql1 +# externalMysqlPort: 33060 +# externalMysqlDatabase: eggroll_meta1 +# externalMysqlUser: fate1 +# externalMysqlPassword: fate_dev1 + + +# servingIp: 192.168.0.1 +# servingPort: 30095 +# serving: + # useRegistry: false + # zookeeper: + # hosts: + # - serving-zookeeper.fate-serving-9999:2181 + # use_acl: false + + +# spark: + # master: + # Image: "federatedai/spark-master" + # ImageTag: "1.6.1-release" + # replicas: 1 + # resources: + # requests: + # cpu: "1" + # memory: "2Gi" + # limits: + # cpu: "1" + # memory: "2Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30977 + # worker: + # Image: "federatedai/spark-worker" + # ImageTag: "1.6.1-release" + # replicas: 2 + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +# hdfs: + # namenode: + # image: federatedai/hadoop-namenode + # imageTag: 2.0.0-hadoop2.7.4-java8 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: hdfs + # datanode: + # image: federatedai/hadoop-datanode + # imageTag: 2.0.0-hadoop2.7.4-java8 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: hdfs +# nginx: + # image: federatedai/nginx + # imageTag: 1.6.1-release + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # loadBalancerIP: + # httpNodePort: 30093 + # grpcNodePort: 30098 + # exchange: + # ip: 192.168.10.1 + # httpPort: 30003 + # grpcPort: 30008 + # route_table: + # 10000: + # proxy: + # - host: 192.168.0.1 + # http_port: 30103 + # grpc_port: 30108 + # fateflow: + # - host: 192.168.0.1 + # http_port: 30107 + # grpc_port: 30102 + + +# pulsar: + # image: federatedai/pulsar + # imageTag: 2.7.0 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: pulsar + # httpNodePort: 30094 + # httpsNodePort: 30099 + # loadBalancerIP: + # publicLB: + # enabled: false + # exchange: + # ip: 192.168.10.1 + # port: 30000 + # domain: fate.org + # route_table: + # 9999: + # host: pulsar + # port: 6650 + # sslPort:6651 + # 10000: + # host: 192.168.10.1 + # port: 30105 + # sslPort: 30109 + # proxy: "" + +# postgres: + # image: postgres + # imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + # user: site_portal + # password: site_portal + # db: site_portal + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + +# frontend: + # image: federatedai/site-portal-frontend + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: + # loadBalancerIP: + +# sitePortalServer: + # image: federatedai/site-portal-server + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # existingClaim: "" + # storageClass: "sitePortalServer" + # accessMode: ReadWriteOnce + # size: 1Gi + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: site_portal + # postgresUser: site_portal + # postgresPassword: site_portal + # adminPassword: admin + # userPassword: user + # serverCert: /var/lib/site-portal/cert/server.crt + # serverKey: /var/lib/site-portal/cert/server.key + # clientCert: /var/lib/site-portal/cert/client.crt + # clientKey: /var/lib/site-portal/cert/client.key + # caCert: /var/lib/site-portal/cert/ca.crt + # tlsEnabled: 'true' + # tlsPort: 8443 + # tlsCommonName: site-1.server.example.com diff --git a/helm-charts/charts/fate/values-template.yaml b/helm-charts/charts/fate/values-template.yaml new file mode 100644 index 00000000..e0ac5487 --- /dev/null +++ b/helm-charts/charts/fate/values-template.yaml @@ -0,0 +1,533 @@ + +image: + registry: {{ .registry | default "" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +partyId: {{ .partyId | int64 | toString }} +partyName: {{ .name }} + +{{- with .istio }} +istio: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .ingress }} +ingress: + {{- with .fateboard }} + fateboard: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .client }} + client: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .spark }} + spark: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .pulsar }} + pulsar: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- if not .tlsEnabled}} + {{- with .frontend }} + frontend: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +persistence: + enabled: {{ .persistence | default "false" }} + +modules: + + + python: + include: {{ has "python" .modules }} + backend: {{ default "spark" .backend }} + {{- with .python }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + serviceAccountName: {{ .serviceAccountName }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + enabledNN: {{ .enabledNN | default false }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "python" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .clustermanager }} + clustermanager: + cores_per_node: {{ .cores_per_node }} + nodes: {{ .nodes }} + {{- end }} + {{- with .spark }} + + spark: +{{ toYaml . | indent 6}} + {{- end }} + {{- with .hdfs }} + hdfs: + name_node: {{ .name_node }} + path_prefix: {{ .path_prefix }} + {{- end }} + {{- with .pulsar }} + pulsar: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + {{- end }} + {{- with .rabbitmq }} + rabbitmq: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + user: {{ .user }} + password: {{ .password }} + {{- end }} + {{- with .nginx }} + nginx: + host: {{ .host }} + http_port: {{ .http_port }} + grpc_port: {{ .grpc_port }} + {{- end }} + {{- end }} + + + fateboard: + include: {{ has "fateboard" .modules }} + {{- with .fateboard }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type }} + username: {{ .username }} + password: {{ .password }} + {{- end}} + + + client: + include: {{ has "client" .modules }} + {{- with .client }} + image: {{ .image }} + imageTag: {{ .imageTag }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "client" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + mysql: + include: {{ has "mysql" .modules }} + {{- with .mysql }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type | default "ClusterIP" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + ip: {{ .ip | default "mysql" }} + port: {{ .port | default "3306" }} + database: {{ .database | default "eggroll_meta" }} + user: {{ .user | default "fate" }} + password: {{ .password | default "fate_dev" }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- end }} + + serving: + ip: {{ .servingIp }} + port: {{ .servingPort }} + {{- with .serving }} + useRegistry: {{ .useRegistry | default false }} + zookeeper: +{{ toYaml .zookeeper | indent 6 }} + {{- end}} + + spark: + include: {{ has "spark" .modules }} + {{- with .spark }} + {{- if .master }} + master: + image: "{{ .master.image }}" + imageTag: "{{ .master.imageTag }}" + replicas: {{ .master.replicas }} + {{- with .master.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .master.type | default "ClusterIP" }} + {{- end }} + {{- if .worker }} + worker: + image: "{{ .worker.image }}" + imageTag: "{{ .worker.imageTag }}" + replicas: {{ .worker.replicas }} + {{- with .worker.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .worker.type | default "ClusterIP" }} + {{- end }} + {{- end }} + + + hdfs: + include: {{ has "hdfs" .modules }} + {{- with .hdfs }} + namenode: + image: {{ .namenode.image }} + imageTag: {{ .namenode.imageTag }} + {{- with .namenode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .namenode.type | default "ClusterIP" }} + nodePort: {{ .namenode.nodePort }} + existingClaim: {{ .namenode.existingClaim }} + storageClass: {{ .namenode.storageClass | default "" }} + accessMode: {{ .namenode.accessMode | default "ReadWriteOnce" }} + size: {{ .namenode.size | default "1Gi" }} + datanode: + image: {{ .datanode.image }} + imageTag: {{ .datanode.imageTag }} + {{- with .datanode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .datanode.type | default "ClusterIP" }} + existingClaim: {{ .datanode.existingClaim }} + storageClass: {{ .datanode.storageClass | default "" }} + accessMode: {{ .datanode.accessMode | default "ReadWriteOnce" }} + size: {{ .datanode.size | default "1Gi" }} + {{- end }} + + + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + httpPort: {{ .httpPort }} + grpcPort: {{ .grpcPort }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + + pulsar: + include: {{ has "pulsar" .modules }} + {{- with .pulsar }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + httpsNodePort: {{ .httpsNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .publicLB}} + publicLB: + enabled: {{ .enabled | default false }} + {{- end }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + port: {{ .port }} + domain: {{ .domain | default "fate.org" }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- end }} + + + postgres: + include: {{ has "postgres" .modules }} + {{- with .postgres }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + user: {{ .user }} + password: {{ .password }} + db: {{ .db }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode }} + size: {{ .size }} + {{- end }} + frontend: + include: {{ has "frontend" .modules }} + {{- with .frontend }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }} + + sitePortalServer: + include: {{ has "sitePortalServer" .modules }} + {{- with .sitePortalServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + postgresHost: {{ .postgresHost | default "postgres" }} + postgresPort: {{ .postgresPort | default "5432" }} + postgresDb: {{ .postgresDb | default "site_portal" }} + postgresUser: {{ .postgresUser | default "site_portal" }} + postgresPassword: {{ .postgresPassword | default "site_portal" }} + adminPassword: {{ .adminPassword | default "admin" }} + userPassword: {{ .userPassword | default "user" }} + serverCert: {{ .serverCert| default "/var/lib/site-portal/cert/server.crt" }} + serverKey: {{ .serverKey | default "/var/lib/site-portal/cert/server.key" }} + clientCert: {{ .clientCert | default "/var/lib/site-portal/cert/client.crt" }} + clientKey: {{ .clientKey | default "/var/lib/site-portal/cert/client.key" }} + caCert: {{ .caCert | default "/var/lib/site-portal/cert/ca.crt" }} + tlsEnabled: {{ .tlsEnabled | default "'true'" }} + tlsPort: {{ .tlsPort | default "8443" }} + tlsCommonName: {{ .tlsCommonName | default "site-1.server.example.com" }} + {{- end }} + + +externalMysqlIp: {{ .externalMysqlIp }} +externalMysqlPort: {{ .externalMysqlPort }} +externalMysqlDatabase: {{ .externalMysqlDatabase }} +externalMysqlUser: {{ .externalMysqlUser }} +externalMysqlPassword: {{ .externalMysqlPassword }} diff --git a/helm-charts/charts/fate/values.yaml b/helm-charts/charts/fate/values.yaml new file mode 100644 index 00000000..1dfc6cc9 --- /dev/null +++ b/helm-charts/charts/fate/values.yaml @@ -0,0 +1,331 @@ + +image: + registry: + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +partyId: 9999 +partyName: fate-9999 + +istio: + enabled: false + +podSecurityPolicy: + enabled: false + +ingress: + fateboard: + # annotations: + hosts: + - name: fateboard.kubefate.net + path: / + tls: [] + # - secretName: my-tls-secret + # hosts: + # - fateboard.kubefate.net + client: + # annotations: + hosts: + - name: notebook.kubefate.net + path: / + tls: [] + spark: + # annotations: + hosts: + - name: spark.kubefate.net + path: / + tls: [] + pulsar: + # annotations: + hosts: + - name: pulsar.kubefate.net + path: / + tls: [] + frontend: + # annotations: + hosts: + - name: frontend.example.com + path: / + tls: [] + +persistence: + enabled: false + +modules: + python: + image: federatedai/python-spark + imageTag: 1.6.1-release + include: true + type: ClusterIP + httpNodePort: 30097 + grpcNodePort: 30092 + loadBalancerIP: + serviceAccountName: + nodeSelector: + tolerations: + affinity: + backend: eggroll + enabledNN: false + # subPath: "" + existingClaim: "" + claimName: python-data + storageClass: "python" + accessMode: ReadWriteOnce + size: 1Gi + clustermanager: + cores_per_node: 16 + nodes: 2 + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: fateflow + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: + rabbitmq: + host: rabbitmq + mng_port: 15672 + port: 5672 + user: fate + password: fate + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + + fateboard: + include: true + image: federatedai/fateboard + imageTag: 1.6.1-release + type: ClusterIP + username: admin + password: admin + + client: + include: true + image: federatedai/client + imageTag: 1.6.1-release + ip: client + type: ClusterIP + nodeSelector: + tolerations: + affinity: + subPath: "client" + existingClaim: "" + storageClass: "client" + accessMode: ReadWriteOnce + size: 1Gi + + mysql: + include: true + image: mysql + imageTag: 8 + type: ClusterIP + nodeSelector: + tolerations: + affinity: + ip: mysql + port: 3306 + database: eggroll_meta + user: fate + password: fate_dev + subPath: "mysql" + existingClaim: "" + claimName: mysql-data + storageClass: "mysql" + accessMode: ReadWriteOnce + size: 1Gi + + serving: + ip: 192.168.9.1 + port: 30095 + useRegistry: false + zookeeper: + hosts: + - serving-zookeeper.fate-serving-9999:2181 + use_acl: false + + spark: + include: true + master: + image: "federatedai/spark-master" + imageTag: "1.6.1-release" + replicas: 1 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30977 + worker: + image: "federatedai/spark-worker" + imageTag: "1.6.1-release" + replicas: 2 + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + nodeSelector: + tolerations: + affinity: + type: ClusterIP + hdfs: + include: true + namenode: + image: federatedai/hadoop-namenode + imageTag: 2.0.0-hadoop2.7.4-java8 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30900 + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: hdfs + datanode: + image: federatedai/hadoop-datanode + imageTag: 2.0.0-hadoop2.7.4-java8 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: hdfs + nginx: + include: true + image: federatedai/nginx + imageTag: 1.6.1-release + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30093 + grpcNodePort: 30098 + loadBalancerIP: + exchange: + ip: 192.168.10.1 + httpPort: 30003 + grpcPort: 30008 + route_table: +# 10000: +# proxy: +# - host: 192.168.10.1 +# http_port: 30103 +# grpc_port: 30108 +# fateflow: +# - host: 192.168.10.1 +# http_port: 30107 +# grpc_port: 30102 + + pulsar: + include: true + image: federatedai/pulsar + imageTag: 2.7.0 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30094 + httpsNodePort: 30099 + loadBalancerIP: + publicLB: + enabled: false + existingClaim: "" + storageClass: "pulsar" + accessMode: ReadWriteOnce + size: 1Gi + # exchange: + # ip: 192.168.10.1 + # port: 30000 + # domain: fate.org + route_table: +# 10000: +# host: 192.168.10.1 +# port: 30104 +# sslPort: 30109 +# proxy: "" +# + + postgres: + include: true + image: postgres + imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: + user: site_portal + password: site_portal + db: site_portal + # subPath: "" + existingClaim: "" + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi + + frontend: + include: true + image: federatedai/site-portal-frontend + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + + # nodePort: + # loadBalancerIP: + + sitePortalServer: + include: true + image: federatedai/site-portal-server + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: + existingClaim: "" + storageClass: "sitePortalServer" + accessMode: ReadWriteOnce + size: 1Gi + postgresHost: postgres + postgresPort: 5432 + postgresDb: site_portal + postgresUser: site_portal + postgresPassword: site_portal + adminPassword: admin + userPassword: user + serverCert: /var/lib/site-portal/cert/server.crt + serverKey: /var/lib/site-portal/cert/server.key + clientCert: /var/lib/site-portal/cert/client.crt + clientKey: /var/lib/site-portal/cert/client.key + caCert: /var/lib/site-portal/cert/ca.crt + tlsEnabled: 'true' + tlsPort: 8443 + tlsCommonName: site-1.server.example.com + + +# externalMysqlIp: mysql +# externalMysqlPort: 3306 +# externalMysqlDatabase: eggroll_meta +# externalMysqlUser: fate +# externalMysqlPassword: fate_dev \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/.helmignore b/helm-charts/charts/openfl-director/.helmignore new file mode 100644 index 00000000..7017e20b --- /dev/null +++ b/helm-charts/charts/openfl-director/.helmignore @@ -0,0 +1,7 @@ +# comment +.git +*/temp* +*/*/temp* +temp? + +*exe \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/Chart.yaml b/helm-charts/charts/openfl-director/Chart.yaml new file mode 100644 index 00000000..a34a4915 --- /dev/null +++ b/helm-charts/charts/openfl-director/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +appVersion: "openfl-director-v0.1.0" +description: A Helm chart for openfl director, based on official OpenFL container image +name: openfl-director +version: v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/_helpers.tpl b/helm-charts/charts/openfl-director/templates/_helpers.tpl new file mode 100644 index 00000000..c09040eb --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/_helpers.tpl @@ -0,0 +1,39 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{/* Helm required labels */}} +{{- define "openfl-director.labels" -}} +name: {{ .Values.name | quote }} +owner: kubefate +cluster: openfl-director +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +{{- end -}} + +{{/* matchLabels */}} +{{- define "openfl-director.matchLabels" -}} +name: {{ .Values.name | quote }} +{{- end -}} + +{{/* +Create the name of the controller service account to use +*/}} +{{- define "serviceAccountName" -}} +{{- if .Values.podSecurityPolicy.enabled -}} + {{ default .Values.name }} +{{- else -}} + {{ default "default" }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/director/configmap.yaml b/helm-charts/charts/openfl-director/templates/director/configmap.yaml new file mode 100644 index 00000000..7bee102e --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/director/configmap.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +data: + director_config.yaml: | + settings: + sample_shape: {{.Values.modules.director.sampleShape}} + target_shape: {{.Values.modules.director.targetShape}} + envoy_health_check_period: {{.Values.modules.director.envoyHealthCheckPeriod}} + listen_host: 0.0.0.0 # listen FQDN or ip +kind: ConfigMap +metadata: + labels: + openfl: director +{{ include "openfl-director.labels" . | indent 4 }} + name: director-config \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/director/deployment.yaml b/helm-charts/charts/openfl-director/templates/director/deployment.yaml new file mode 100644 index 00000000..d0738cc2 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/director/deployment.yaml @@ -0,0 +1,82 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + openfl: director +{{ include "openfl-director.labels" . | indent 4 }} + name: director +spec: + replicas: 1 + selector: + matchLabels: + openfl: director +{{ include "openfl-director.matchLabels" . | indent 6 }} + strategy: + type: Recreate + template: + metadata: + labels: + openfl: director +{{ include "openfl-director.labels" . | indent 8 }} + spec: + containers: + - command: + - /bin/bash + - -c + - | + mkdir -p workspace/logs + cd workspace + fx director start --director-config-path director_config.yaml --root-cert-path cert/root_ca.crt --private-key-path cert/priv.key --public-cert-path cert/director.crt + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.director.image | default "fedlcm-openfl" }}:{{ .Values.modules.director.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: director + ports: + - containerPort: 50051 + name: listen-port + protocol: TCP + volumeMounts: + - mountPath: /openfl/workspace/director_config.yaml + name: director-config + readOnly: true + subPath: director_config.yaml + - mountPath: /openfl/workspace/cert/ + name: director-cert + workingDir: /openfl + {{- with .Values.modules.director.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.director.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.director.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + volumes: + - configMap: + name: director-config + name: director-config + - name: director-cert + secret: + secretName: director-cert \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/director/service.yaml b/helm-charts/charts/openfl-director/templates/director/service.yaml new file mode 100644 index 00000000..8c2a7de0 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/director/service.yaml @@ -0,0 +1,40 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: Service +metadata: + labels: + openfl: director +{{ include "openfl-director.labels" . | indent 4 }} + name: director +spec: + ports: + - name: director + port: 50051 + protocol: TCP + targetPort: listen-port + {{- if eq .Values.modules.director.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.director.nodePort }} + {{- end }} + - name: agg + port: 50052 + protocol: TCP + targetPort: 50052 + selector: + openfl: director + type: {{ .Values.modules.director.type }} + {{- if .Values.modules.director.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.director.loadBalancerIP }}" + {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/ingress.yaml b/helm-charts/charts/openfl-director/templates/ingress.yaml new file mode 100644 index 00000000..1fa91844 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/ingress.yaml @@ -0,0 +1,43 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notebook + labels: + openfl: notebook +{{ include "openfl-director.labels" . | indent 4 }} +{{- if .Values.ingress.notebook.annotations }} + annotations: +{{ toYaml .Values.ingress.notebook.annotations | indent 4 }} +{{- end }} +spec: + rules: + {{- range .Values.ingress.notebook.hosts }} + - host: {{ .name }} + http: + paths: + - path: {{ default "/" .path }} + pathType: Prefix + backend: + service: + name: notebook + port: + number: 8888 + {{- end }} +{{- if .Values.ingress.notebook.tls }} + tls: +{{ toYaml .Values.ingress.notebook.tls | indent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml b/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml new file mode 100644 index 00000000..ff13859f --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml @@ -0,0 +1,75 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + openfl: notebook +{{ include "openfl-director.labels" . | indent 4 }} + name: notebook +spec: + replicas: 1 + selector: + matchLabels: + openfl: notebook +{{ include "openfl-director.matchLabels" . | indent 6 }} + strategy: + type: Recreate + template: + metadata: + labels: + openfl: notebook +{{ include "openfl-director.labels" . | indent 8 }} + spec: + containers: + - command: + - /bin/bash + - -c + - | + jupyter lab --notebook-dir /usr/local/lib/python3.8/dist-packages/openfl-tutorials --allow-root --ip=0.0.0.0 --NotebookApp.token='' --NotebookApp.password={{ .Values.modules.notebook.password }} + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.notebook.image | default "fedlcm-openfl" }}:{{ .Values.modules.notebook.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: notebook + ports: + - containerPort: 8888 + name: jupyter-port + protocol: TCP + volumeMounts: + - mountPath: /openfl/workspace/cert/ + name: notebook-cert + workingDir: /openfl/workspace + dnsPolicy: ClusterFirst + {{- with .Values.modules.notebook.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.notebook.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.notebook.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 8 }} + {{- end }} + serviceAccountName: {{ template "serviceAccountName" . }} + restartPolicy: Always + volumes: + - name: notebook-cert + secret: + secretName: notebook-cert \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/notebook/service.yaml b/helm-charts/charts/openfl-director/templates/notebook/service.yaml new file mode 100644 index 00000000..df1a2e79 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/notebook/service.yaml @@ -0,0 +1,36 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: Service +metadata: + labels: + openfl: notebook +{{ include "openfl-director.labels" . | indent 4 }} + name: notebook +spec: + ports: + - name: notebook + port: 8888 + protocol: TCP + targetPort: jupyter-port + {{- if eq .Values.modules.notebook.type "NodePort" "LoadBalancer" }} + nodePort: {{ .Values.modules.notebook.nodePort }} + {{- end }} + selector: + openfl: notebook + type: {{ .Values.modules.notebook.type }} + {{- if .Values.modules.notebook.loadBalancerIP }} + loadBalancerIP: "{{ .Values.modules.notebook.loadBalancerIP }}" + {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/psp.yaml b/helm-charts/charts/openfl-director/templates/psp.yaml new file mode 100644 index 00000000..a4f710f5 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/psp.yaml @@ -0,0 +1,35 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + labels: + openfl: psp +{{ include "openfl-director.labels" . | indent 4 }} + name: {{ .Values.name }} +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/templates/role.yaml b/helm-charts/charts/openfl-director/templates/role.yaml new file mode 100644 index 00000000..46912674 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/role.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + openfl: role +{{ include "openfl-director.labels" . | indent 4 }} + name: {{ .Values.name }} +rules: + - apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ .Values.name }}] +{{- end }} diff --git a/helm-charts/charts/openfl-director/templates/rolebinding.yaml b/helm-charts/charts/openfl-director/templates/rolebinding.yaml new file mode 100644 index 00000000..909dec8e --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/rolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + openfl: serviceAccount +{{ include "openfl-director.labels" . | indent 4 }} + name: {{ .Values.name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Values.name }} +subjects: + - kind: ServiceAccount + name: {{ .Values.name }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm-charts/charts/openfl-director/templates/serviceaccount.yaml b/helm-charts/charts/openfl-director/templates/serviceaccount.yaml new file mode 100644 index 00000000..bf6154d6 --- /dev/null +++ b/helm-charts/charts/openfl-director/templates/serviceaccount.yaml @@ -0,0 +1,23 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + openfl: serviceAccount +{{ include "openfl-director.labels" . | indent 4 }} + name: {{ .Values.name }} +{{- end }} diff --git a/helm-charts/charts/openfl-director/values-template-example.yaml b/helm-charts/charts/openfl-director/values-template-example.yaml new file mode 100644 index 00000000..9cd71af0 --- /dev/null +++ b/helm-charts/charts/openfl-director/values-template-example.yaml @@ -0,0 +1,49 @@ +name: openfl-director +namespace: openfl-director +chartName: openfl-director +chartVersion: v0.1.0 +registry: "federatedai" +imageTag: "v0.1.0" +pullPolicy: IfNotPresent +# imagePullSecrets: +# - name: myregistrykey +podSecurityPolicy: + enabled: false +modules: + - director + - notebook + +# ingress: + # notebook: + # annotations: + # hosts: + # - name: notebook.openfl.example.com + # path: / + # tls: + # - secretName: notebook-cert + # hosts: + # - notebook.openfl.example.com + +#director: +# image: fedlcm-openfl +# imageTag: v0.1.0 +# sampleShape: "['784']" +# targetShape: "['1']" +# envoyHealthCheckPeriod: 60 +# nodeSelector: +# tolerations: +# affinity: +# type: NodePort +# nodePort: +# loadBalancerIp: + +#notebook: +# image: fedlcm-openfl +# imageTag: v0.1.0 +# password: argon2:$argon2id$v=19$m=10240,t=10,p=8$TmW50aM7Fey2lNrU7kpOhQ$s4SY7l8QItxgR9iwVA+DTc2uwGnawh1p1dB42bbLH48 +# nodeSelector: +# tolerations: +# affinity: +# type: NodePort +# nodePort: +# loadBalancerIp: \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/values-template.yaml b/helm-charts/charts/openfl-director/values-template.yaml new file mode 100644 index 00000000..f7e6a50c --- /dev/null +++ b/helm-charts/charts/openfl-director/values-template.yaml @@ -0,0 +1,78 @@ +name: {{ .name }} + +image: + registry: {{ .registry | default "federatedai" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .ingress }} +ingress: + {{- with .notebook }} + notebook: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} +{{- end }} + +modules: + director: + {{- with .director }} + image: {{ .image }} + sampleShape: "{{ .sampleShape }}" + targetShape: "{{ .targetShape }}" + envoyHealthCheckPeriod: {{ .envoyHealthCheckPeriod | default "60"}} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "NodePort" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }} + + notebook: + {{- with .notebook}} + image: {{ .image }} + password: {{ .password }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "NodePort" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-director/values.yaml b/helm-charts/charts/openfl-director/values.yaml new file mode 100644 index 00000000..5da4dda9 --- /dev/null +++ b/helm-charts/charts/openfl-director/values.yaml @@ -0,0 +1,41 @@ +image: + registry: federatedai + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +ingress: + notebook: + # annotations: + hosts: + - name: notebook.openfl.example.com + path: / + tls: [] + +modules: + director: + image: fedlcm-openfl + imageTag: v0.1.0 + sampleShape: "['1']" + targetShape: "['1']" + envoyHealthCheckPeriod: 60 + # nodeSelector: + # tolerations: + # affinity: + type: NodePort + # nodePort: + # loadBalancerIP: + + notebook: + image: fedlcm-openfl + imageTag: v0.1.0 + # password: + # nodeSelector: + # tolerations: + # affinity: + type: NodePort + # nodePort: + # loadBalancerIP: \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/.helmignore b/helm-charts/charts/openfl-envoy/.helmignore new file mode 100644 index 00000000..7017e20b --- /dev/null +++ b/helm-charts/charts/openfl-envoy/.helmignore @@ -0,0 +1,7 @@ +# comment +.git +*/temp* +*/*/temp* +temp? + +*exe \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/Chart.yaml b/helm-charts/charts/openfl-envoy/Chart.yaml new file mode 100644 index 00000000..22698160 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +appVersion: "openfl-envoy-v0.1.0" +description: A Helm chart for openfl envoy +name: openfl-envoy +version: v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/templates/_helpers.tpl b/helm-charts/charts/openfl-envoy/templates/_helpers.tpl new file mode 100644 index 00000000..9c9b16c8 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/_helpers.tpl @@ -0,0 +1,39 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{/* Helm required labels */}} +{{- define "openfl-envoy.labels" -}} +name: {{ .Values.name | quote }} +owner: kubefate +cluster: openfl-envoy +heritage: {{ .Release.Service }} +release: {{ .Release.Name }} +chart: {{ .Chart.Name }} +{{- end -}} + +{{/* matchLabels */}} +{{- define "openfl-envoy.matchLabels" -}} +name: {{ .Values.name | quote }} +{{- end -}} + +{{/* +Create the name of the controller service account to use +*/}} +{{- define "serviceAccountName" -}} +{{- if .Values.podSecurityPolicy.enabled -}} + {{ default .Values.name }} +{{- else -}} + {{ default "default" }} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/templates/envoy/configmap.yaml b/helm-charts/charts/openfl-envoy/templates/envoy/configmap.yaml new file mode 100644 index 00000000..92650893 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/envoy/configmap.yaml @@ -0,0 +1,24 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +data: + envoy_config.yaml: | +{{ toYaml .Values.modules.envoy.envoyConfigs | indent 4 }} +kind: ConfigMap +metadata: + labels: +{{ include "openfl-envoy.labels" . | indent 4 }} + openfl: envoy + name: envoy-config \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml b/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml new file mode 100644 index 00000000..27a8e1be --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml @@ -0,0 +1,74 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + openfl: envoy +{{ include "openfl-envoy.labels" . | indent 4 }} + name: envoy +spec: + replicas: 1 + selector: + matchLabels: + openfl: envoy + template: + metadata: + labels: + openfl: envoy + spec: + containers: + - command: + - /bin/bash + - -c + - "pip install -r python/requirements.txt; PYTHONPATH=${PYTHONPATH}:`pwd`/python fx envoy start --shard-name \"$ENVOY_NAME\" --director-host \"$DIRECTOR_FQDN\" --director-port \"$DIRECTOR_PORT\" --root-cert-path cert/root_ca.crt --private-key-path cert/priv.key --public-cert-path cert/envoy.crt --envoy-config-path envoy_config.yaml" + env: + - name: ENVOY_NAME + value: {{ .Values.name }} + - name: DIRECTOR_FQDN + value: {{ .Values.modules.envoy.directorFqdn }} + - name: DIRECTOR_PORT + value: "{{ .Values.modules.envoy.directorPort }}" + - name: AGG_PORT + value: "{{ .Values.modules.envoy.aggPort }}" + image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.envoy.image | default "fedlcm-openfl" }}:{{ .Values.modules.envoy.imageTag | default "latest"}} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: envoy + volumeMounts: + - mountPath: /openfl/workspace/envoy_config.yaml + name: envoy-config + readOnly: true + subPath: envoy_config.yaml + - mountPath: /openfl/workspace/python/ + name: envoy-python-configs + readOnly: true + - mountPath: /openfl/workspace/cert/ + name: envoy-cert + workingDir: /openfl/workspace + hostAliases: + - hostnames: + - {{ .Values.modules.envoy.directorFqdn }} + ip: {{ .Values.modules.envoy.directorIp }} + restartPolicy: Always + volumes: + - configMap: + name: envoy-config + name: envoy-config + - configMap: + name: envoy-python-configs + name: envoy-python-configs + - name: envoy-cert + secret: + secretName: envoy-cert diff --git a/helm-charts/charts/openfl-envoy/templates/psp.yaml b/helm-charts/charts/openfl-envoy/templates/psp.yaml new file mode 100644 index 00000000..e610ba4a --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/psp.yaml @@ -0,0 +1,35 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + labels: + openfl: psp +{{ include "openfl-envoy.labels" . | indent 4 }} + name: {{ .Values.name }} +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +{{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/templates/role.yaml b/helm-charts/charts/openfl-envoy/templates/role.yaml new file mode 100644 index 00000000..e520e912 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/role.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + openfl: role +{{ include "openfl-envoy.labels" . | indent 4 }} + name: {{ .Values.name }} +rules: + - apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ .Values.name }}] +{{- end }} diff --git a/helm-charts/charts/openfl-envoy/templates/rolebinding.yaml b/helm-charts/charts/openfl-envoy/templates/rolebinding.yaml new file mode 100644 index 00000000..4c474ba4 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/rolebinding.yaml @@ -0,0 +1,31 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + openfl: serviceAccount +{{ include "openfl-envoy.labels" . | indent 4 }} + name: {{ .Values.name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Values.name }} +subjects: + - kind: ServiceAccount + name: {{ .Values.name }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm-charts/charts/openfl-envoy/templates/serviceaccount.yaml b/helm-charts/charts/openfl-envoy/templates/serviceaccount.yaml new file mode 100644 index 00000000..81ab8bd7 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/templates/serviceaccount.yaml @@ -0,0 +1,23 @@ +# Copyright 2022 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podSecurityPolicy.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + openfl: serviceAccount +{{ include "openfl-envoy.labels" . | indent 4 }} + name: {{ .Values.name }} +{{- end }} diff --git a/helm-charts/charts/openfl-envoy/values-template-example.yaml b/helm-charts/charts/openfl-envoy/values-template-example.yaml new file mode 100644 index 00000000..1eba47d9 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/values-template-example.yaml @@ -0,0 +1,31 @@ +name: envoy-1 +namespace: openfl-envoy-1 +chartName: openfl-envoy +chartVersion: v0.1.0 +registry: "federatedai" +pullPolicy: IfNotPresent +imagePullSecrets: + - name: myregistrykey +podSecurityPolicy: + enabled: false +modules: + - envoy + +#envoy: +# image: fedlcm-openfl +# imageTag: v0.1.0 +# directorFqdn: director.openfl.example.com +# directorIp: 192.168.1.1. +# directorPort: 50051 +# aggPort: 50052 +# envoyConfigs: +# params: +# cuda_devices: [] +# optional_plugin_components: {} +# shard_descriptor: +# template: mnist_shard_descriptor.MnistShardDescriptor +# params: +# rank_worldsize: 1, 2 +# nodeSelector: +# tolerations: +# affinity: \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/values-template.yaml b/helm-charts/charts/openfl-envoy/values-template.yaml new file mode 100644 index 00000000..226ce5bb --- /dev/null +++ b/helm-charts/charts/openfl-envoy/values-template.yaml @@ -0,0 +1,39 @@ +name: {{ .name }} + +image: + registry: {{ .registry | default "federatedai" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +modules: + envoy: + {{- with .envoy }} + image: {{ .image }} + imageTag: {{ .imageTag }} + directorFqdn: {{ .directorFqdn }} + directorIp: {{ .directorIp }} + directorPort: {{ .directorPort }} + aggPort: {{ .aggPort }} + envoyConfigs: +{{ toYaml .envoyConfigs | indent 6 }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/openfl-envoy/values.yaml b/helm-charts/charts/openfl-envoy/values.yaml new file mode 100644 index 00000000..84627c93 --- /dev/null +++ b/helm-charts/charts/openfl-envoy/values.yaml @@ -0,0 +1,23 @@ +name: envoy-1 + +image: + registry: federatedai + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +modules: + envoy: + image: fedlcm-openfl + imageTag: v0.1.0 + directorFqdn: director + directorIp: 192.168.1.1 + directorPort: 50051 + aggPort: 50052 + envoyConfigs: + # nodeSelector: + # tolerations: + # affinity: \ No newline at end of file diff --git a/helm-charts/fml-manager.yaml b/helm-charts/fml-manager.yaml new file mode 100644 index 00000000..ee560f52 --- /dev/null +++ b/helm-charts/fml-manager.yaml @@ -0,0 +1,104 @@ +name: fml-manager +namespace: fml-manager +chartName: fate-exchange +chartVersion: v1.6.1-fedlcm-v0.1.0 +partyId: 0 +registry: "" +pullPolicy: +imagePullSecrets: +- name: myregistrykey +persistence: false +istio: + enabled: false +podSecurityPolicy: + enabled: false +modules: + - trafficServer + - nginx + - postgres + - fmlManagerServer + +# trafficServer: + # image: federatedai/trafficServer + # imageTag: latest + # replicas: 3 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # nodePort: 30007 + # loadBalancerIP: 192.168.0.1 + # route_table: + # sni: + # - fqdn: 10000.fate.org + # tunnelRoute: 192.168.0.2:30109 + # - fqdn: 9999.fate.org + # tunnelRoute: 192.168.0.3:30099 + +# nginx: + # image: federatedai/nginx + # imageTag: 1.6.1-release + # replicas: 3 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # httpNodePort: 30003 + # grpcNodePort: 30008 + # loadBalancerIP: 192.168.0.1 + # route_table: + # 9999: + # proxy: + # - host: 192.168.9.1 + # http_port: 30093 + # grpc_port: 30098 + # fateflow: + # - host: 192.168.9.1 + # http_port: 30097 + # grpc_port: 30092 + # 10000: + # proxy: + # - host: 192.168.10.1 + # http_port: 30103 + # grpc_port: 30108 + # fateflow: + # - host: 192.168.10.1 + # http_port: 30107 + # grpc_port: 30102 + +# postgres: + # image: postgres + # imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + # user: fml_manager + # password: fml_manager + # db: fml_manager + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + +# fmlManagerServer: + # image: federatedai/fml-manager-server + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # type: NodePort + # nodePort: + # loadBalancerIP: 192.168.0.1 + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: fml_manager + # postgresUser: fml_manager + # postgresPassword: fml_manager + # tlsPort: 8443 + # serverCert: /var/lib/fml_manager/cert/server.crt + # serverKey: /var/lib/fml_manager/cert/server.key + # clientCert: /var/lib/fml_manager/cert/client.crt + # clientKey: /var/lib/fml_manager/cert/client.key + # caCert: /var/lib/fml_manager/cert/ca.crt + # tlsEnabled: 'true' \ No newline at end of file diff --git a/helm-charts/site-portal.yaml b/helm-charts/site-portal.yaml new file mode 100644 index 00000000..1f448c46 --- /dev/null +++ b/helm-charts/site-portal.yaml @@ -0,0 +1,304 @@ +name: site-portal-9999 +namespace: site-portal-9999 +chartName: fate +chartVersion: v1.6.1-fedlcm-v0.1.0 +partyId: 9999 +registry: "" +pullPolicy: IfNotPresent +imagePullSecrets: +- name: myregistrykey +persistence: false +istio: + enabled: false +podSecurityPolicy: + enabled: false +modules: + - mysql + - python + - fateboard + - client + - frontend + - sitePortalServer + - postgres + - spark + - hdfs + - nginx + - pulsar + +ingress: + fateboard: + hosts: + - name: party9999.fateboard.example.com + client: + hosts: + - name: party9999.notebook.example.com + spark: + hosts: + - name: party9999.spark.example.com + frontend: + hosts: + - name: party9999.frontend.example.com + +# python: + # image: federatedai/python-spark + # imageTag: 1.6.1-release + # type: NodePort + # httpNodePort: 30097 + # grpcNodePort: 30092 + # loadBalancerIP: + # serviceAccountName: "" + # resources: + # nodeSelector: + # tolerations: + # affinity: + # enabledNN: false + # existingClaim: "" + # storageClass: "python" + # accessMode: ReadWriteMany + # size: 1Gi + # clustermanager: + # cores_per_node: 16 + # nodes: 2 + # spark: + # cores_per_node: 20 + # nodes: 2 + # master: spark://spark-master:7077 + # driverHost: + # driverHostType: + # portMaxRetries: + # driverStartPort: + # blockManagerStartPort: + # pysparkPython: + # hdfs: + # name_node: hdfs://namenode:9000 + # path_prefix: + # rabbitmq: + # host: rabbitmq + # mng_port: 15672 + # port: 5672 + # user: fate + # password: fate + # pulsar: + # host: pulsar + # mng_port: 8080 + # port: 6650 + # nginx: + # host: nginx + # http_port: 9300 + # grpc_port: 9310 + +# fateboard: + # image: federatedai/fateboard + # imageTag: 1.6.1-release + # type: ClusterIP + # username: admin + # password: admin + +# client: + # image: federatedai/client + # imageTag: 1.6.1-release + # nodeSelector: + # subPath: "" + # existingClaim: "" + # storageClass: "client" + # accessMode: ReadWriteOnce + # size: 1Gi + +# mysql: + # image: mysql + # imageTag: 8 + # nodeSelector: + # tolerations: + # affinity: + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + # subPath: "" + # existingClaim: "" + # storageClass: "mysql" + # accessMode: ReadWriteOnce + # size: 1Gi + + +# externalMysqlIp: mysql1 +# externalMysqlPort: 33060 +# externalMysqlDatabase: eggroll_meta1 +# externalMysqlUser: fate1 +# externalMysqlPassword: fate_dev1 + + +# servingIp: 192.168.0.1 +# servingPort: 30095 +# serving: + # useRegistry: false + # zookeeper: + # hosts: + # - serving-zookeeper.fate-serving-9999:2181 + # use_acl: false + + +# spark: + # master: + # Image: "federatedai/spark-master" + # ImageTag: "1.6.1-release" + # replicas: 1 + # resources: + # requests: + # cpu: "1" + # memory: "2Gi" + # limits: + # cpu: "1" + # memory: "2Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30977 + # worker: + # Image: "federatedai/spark-worker" + # ImageTag: "1.6.1-release" + # replicas: 2 + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +# hdfs: + # namenode: + # image: federatedai/hadoop-namenode + # imageTag: 2.0.0-hadoop2.7.4-java8 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: hdfs + # datanode: + # image: federatedai/hadoop-datanode + # imageTag: 2.0.0-hadoop2.7.4-java8 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: hdfs +# nginx: + # image: federatedai/nginx + # imageTag: 1.6.1-release + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # loadBalancerIP: + # httpNodePort: 30093 + # grpcNodePort: 30098 + # exchange: + # ip: 192.168.10.1 + # httpPort: 30003 + # grpcPort: 30008 + # route_table: + # 10000: + # proxy: + # - host: 192.168.0.1 + # http_port: 30103 + # grpc_port: 30108 + # fateflow: + # - host: 192.168.0.1 + # http_port: 30107 + # grpc_port: 30102 + + +# pulsar: + # image: federatedai/pulsar + # imageTag: 2.7.0 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # httpNodePort: 30094 + # httpsNodePort: 30099 + # loadBalancerIP: + # existingClaim: "" + # accessMode: ReadWriteOnce + # size: 1Gi + # storageClass: pulsar + # publicLB: + # enabled: false + # exchange: + # ip: 192.168.10.1 + # port: 30000 + # domain: fate.org + # route_table: + # 9999: + # host: pulsar + # port: 6650 + # sslPort:6651 + # 10000: + # host: 192.168.10.1 + # port: 30105 + # sslPort: 30109 + # proxy: "" + +# postgres: + # image: postgres + # imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + # user: site_portal + # password: site_portal + # db: site_portal + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + +# frontend: + # image: federatedai/site-portal-frontend + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: + # loadBalancerIP: + +# sitePortalServer: + # image: federatedai/site-portal-server + # imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + # existingClaim: "" + # storageClass: "sitePortalServer" + # accessMode: ReadWriteOnce + # size: 1Gi + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: site_portal + # postgresUser: site_portal + # postgresPassword: site_portal + # adminPassword: admin + # userPassword: user + # serverCert: /var/lib/site-portal/cert/server.crt + # serverKey: /var/lib/site-portal/cert/server.key + # clientCert: /var/lib/site-portal/cert/client.crt + # clientKey: /var/lib/site-portal/cert/client.key + # caCert: /var/lib/site-portal/cert/ca.crt + # tlsEnabled: 'true' + # tlsPort: 8443 + # tlsCommonName: site-1.server.example.com \ No newline at end of file diff --git a/k8s_deploy.yaml b/k8s_deploy.yaml new file mode 100644 index 00000000..db56abb5 --- /dev/null +++ b/k8s_deploy.yaml @@ -0,0 +1,340 @@ +apiVersion: v1 +kind: Secret +metadata: + name: fedlcm-secret + namespace: fedlcm +type: Opaque +stringData: + POSTGRES_PASSWORD: lifecycle_manager + LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD: admin + LIFECYCLEMANAGER_SECRETKEY: passphrase123456 + DOCKER_STEPCA_INIT_PASSWORD: stepca +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: stepca-entrypoint + namespace: fedlcm +data: + entrypoint.sh: | + #!/bin/bash + set -eo pipefail + + export STEPPATH=$(step path) + + declare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES) + + function init_if_possible () { + local missing_vars=0 + for var in "${REQUIRED_INIT_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing_vars=1 + fi + done + if [ ${missing_vars} = 1 ]; then + >&2 echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + else + step_ca_init "${@}" + fi + } + + function generate_password () { + set +o pipefail + < /dev/urandom tr -dc A-Za-z0-9 | head -c40 + echo + set -o pipefail + } + + function step_ca_init () { + local -a setup_args=( + --name "${DOCKER_STEPCA_INIT_NAME}" + --dns "${DOCKER_STEPCA_INIT_DNS_NAMES}" + --provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}" + --password-file "${STEPPATH}/password" + --address ":9000" + ) + if [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password" + else + generate_password > "${STEPPATH}/password" + fi + if [ -n "${DOCKER_STEPCA_INIT_SSH}" ]; then + setup_args=("${setup_args[@]}" --ssh) + fi + step ca init "${setup_args[@]}" + sed -i 's/"authority": {/"authority": { "claims": { "maxTLSCertDuration" : "8760h" },/' ${STEPPATH}/config/ca.json + mv $STEPPATH/password $PWDPATH + } + + if [ ! -f "${STEPPATH}/config/ca.json" ]; then + init_if_possible + fi + + exec "${@}" +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: fedlcm + labels: + app: fedlcm + tier: db +spec: + selector: + app: fedlcm + tier: db + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: fedlcm + labels: + app: fedlcm +spec: + replicas: 1 + selector: + matchLabels: + app: fedlcm + tier: db + template: + metadata: + name: postgres + labels: + app: fedlcm + tier: db + spec: + serviceAccountName: fedlcm-admin + containers: + - name: postgres + image: postgres:13.3 + ports: + - containerPort: 5432 + name: postgresql-db + volumeMounts: + - name: postgres-volume + mountPath: /var/lib/postgresql/data + env: + - name: POSTGRES_USER + value: "lifecycle_manager" + - name: POSTGRES_DB + value: "lifecycle_manager" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: POSTGRES_PASSWORD + resources: + limits: + memory: "4Gi" + cpu: "2" + requests: + memory: 512Mi + cpu: "0.5" + volumes: + - name: postgres-volume + # If you want use persistent storage, please change emptyDir to persistentVolumeClaim. + emptyDir: {} + # persistentVolumeClaim: + # claimName: postgres-data +# --- +# apiVersion: v1 +# kind: PersistentVolumeClaim +# metadata: +# name: postgres-data +# namespace: fedlcm +# spec: +# storageClassName: +# accessModes: +# - ReadWriteOnce +# resources: +# requests: +# storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server + namespace: fedlcm + labels: + app: fedlcm +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: fedlcm + tier: backend + template: + metadata: + labels: + app: fedlcm + tier: backend + spec: + serviceAccountName: fedlcm-admin + containers: + - name: stepca + image: smallstep/step-ca:0.18.2 + imagePullPolicy: IfNotPresent + env: + - name: DOCKER_STEPCA_INIT_NAME + value: "stepca" + - name: DOCKER_STEPCA_INIT_DNS_NAMES + value: "localhost" + - name: DOCKER_STEPCA_INIT_PASSWORD + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: DOCKER_STEPCA_INIT_PASSWORD + - name: DOCKER_STEPCA_INIT_PROVISIONER_NAME + value: "stepca" + - name: STEPCA_DATA_FOLDER + value: "/home/step" + ports: + - containerPort: 9000 + volumeMounts: + - mountPath: /home/step + name: stepca-volume + - mountPath: /entrypoint.sh + subPath: entrypoint.sh + name: stepca-entrypoint + - name: server + image: federatedai/fedlcm-server:v0.1.0 + imagePullPolicy: IfNotPresent + securityContext: + runAsUser: 1000 + env: + - name: POSTGRES_HOST + value: "postgres" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: "lifecycle_manager" + - name: POSTGRES_DB + value: "lifecycle_manager" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: POSTGRES_PASSWORD + - name: LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: LIFECYCLEMANAGER_INITIAL_ADMIN_PASSWORD + - name: LIFECYCLEMANAGER_SECRETKEY + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: LIFECYCLEMANAGER_SECRETKEY + - name: LIFECYCLEMANAGER_BUILTINCA_HOST + value: "localhost" + - name: LIFECYCLEMANAGER_BUILTINCA_PROVISIONER_PASSWORD + valueFrom: + secretKeyRef: + name: fedlcm-secret + key: DOCKER_STEPCA_INIT_PASSWORD + - name: LIFECYCLEMANAGER_BUILTINCA_PROVISIONER_NAME + value: "stepca" + - name: LIFECYCLEMANAGER_BUILTINCA_DATADIR + value: "/home/step" + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /home/step + name: stepca-volume + restartPolicy: Always + volumes: + - name: stepca-volume + # If you want use persistent storage, please change emptyDir to persistentVolumeClaim. + emptyDir: {} + # persistentVolumeClaim: + # claimName: stepca-data + - name: stepca-entrypoint + configMap: + name: stepca-entrypoint +# --- +# apiVersion: v1 +# kind: PersistentVolumeClaim +# metadata: +# name: stepca-data +# namespace: fedlcm +# spec: +# storageClassName: +# accessModes: +# - ReadWriteOnce +# resources: +# requests: +# storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: server + namespace: fedlcm + labels: + app: fedlcm +spec: + selector: + app: fedlcm + tier: backend + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: fedlcm + labels: + app: fedlcm +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: fedlcm + tier: frontend + template: + metadata: + labels: + app: fedlcm + tier: frontend + spec: + serviceAccountName: fedlcm-admin + containers: + - name: frontend + image: federatedai/fedlcm-frontend:v0.1.0 + imagePullPolicy: IfNotPresent + env: + - name: LIFECYCLEMANAGER_SERVER_HOST + value: "server" + ports: + - containerPort: 8080 + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: fedlcm + labels: + app: fedlcm +spec: + type: NodePort + selector: + app: fedlcm + tier: frontend + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + nodePort: 30008 diff --git a/make/frontend/Dockerfile b/make/frontend/Dockerfile new file mode 100644 index 00000000..e828e565 --- /dev/null +++ b/make/frontend/Dockerfile @@ -0,0 +1,27 @@ +FROM node:15.4.0 as builder + +WORKDIR /build_dir + +COPY . . +RUN cd frontend && npm install && cd .. && make frontend + +FROM photon:4.0 + +RUN tdnf install -y nginx shadow gettext >> /dev/null \ + && tdnf clean all \ + && ln -sf /dev/stdout /var/log/nginx/access.log \ + && ln -sf /dev/stderr /var/log/nginx/error.log + +COPY --from=builder /build_dir/output/frontend /usr/share/nginx/html +COPY --from=builder /build_dir/make/frontend/nginx.conf.template /etc/nginx/conf.d/nginx.conf.template + +RUN groupadd -r -g 10000 nginx && useradd --no-log-init -r -g 10000 -u 10000 nginx \ + && chown -R nginx:nginx /etc/nginx \ + && chown -R nginx:nginx /usr/share/nginx/html + +VOLUME /var/cache/nginx /var/log/nginx /run + +STOPSIGNAL SIGQUIT + +USER nginx +CMD ["/bin/bash", "-c", "LIFECYCLEMANAGER_SERVER_HOST=${LIFECYCLEMANAGER_SERVER_HOST:-server} envsubst '${LIFECYCLEMANAGER_SERVER_HOST}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/nginx.conf && exec nginx -g 'daemon off;'"] diff --git a/make/frontend/nginx.conf.template b/make/frontend/nginx.conf.template new file mode 100644 index 00000000..0f3bb301 --- /dev/null +++ b/make/frontend/nginx.conf.template @@ -0,0 +1,52 @@ + +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + upstream server { + server ${LIFECYCLEMANAGER_SERVER_HOST}:8080; + } + + server { + listen 8080; + server_name localhost; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /api/ { + proxy_pass http://server/api/; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_buffering off; + proxy_request_buffering off; + } + } +} \ No newline at end of file diff --git a/make/server/Dockerfile b/make/server/Dockerfile new file mode 100644 index 00000000..b79d22f5 --- /dev/null +++ b/make/server/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.17 as builder + +WORKDIR /workspace + +COPY . . +RUN make server + +FROM photon:4.0 +WORKDIR / +COPY --from=builder /workspace/output . + +RUN tdnf install -y tzdata shadow >> /dev/null \ + && tdnf clean all \ + && groupadd -r -g 10000 manager-server \ + && useradd --no-log-init -r -m -g 10000 -u 10000 manager-server +USER manager-server + +EXPOSE 8080 +ENTRYPOINT ["/lifecycle-manager"] diff --git a/make/stepca/entrypoint.sh b/make/stepca/entrypoint.sh new file mode 100644 index 00000000..6d34fb85 --- /dev/null +++ b/make/stepca/entrypoint.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -eo pipefail + +# Paraphrased from: +# https://github.com/influxdata/influxdata-docker/blob/0d341f18067c4652dfa8df7dcb24d69bf707363d/influxdb/2.0/entrypoint.sh +# (a repo with no LICENSE.md) + +export STEPPATH=$(step path) + +# List of env vars required for step ca init +declare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES) + +# Ensure all env vars required to run step ca init are set. +function init_if_possible () { + local missing_vars=0 + for var in "${REQUIRED_INIT_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing_vars=1 + fi + done + if [ ${missing_vars} = 1 ]; then + >&2 echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + else + step_ca_init "${@}" + fi +} + +function generate_password () { + set +o pipefail + < /dev/urandom tr -dc A-Za-z0-9 | head -c40 + echo + set -o pipefail +} + +# Initialize a CA if not already initialized +function step_ca_init () { + local -a setup_args=( + --name "${DOCKER_STEPCA_INIT_NAME}" + --dns "${DOCKER_STEPCA_INIT_DNS_NAMES}" + --provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}" + --password-file "${STEPPATH}/password" + --address ":9000" + ) + if [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password" + else + generate_password > "${STEPPATH}/password" + fi + if [ -n "${DOCKER_STEPCA_INIT_SSH}" ]; then + setup_args=("${setup_args[@]}" --ssh) + fi + step ca init "${setup_args[@]}" + sed -i 's/"authority": {/"authority": { "claims": { "maxTLSCertDuration" : "8760h" },/' ${STEPPATH}/config/ca.json + mv $STEPPATH/password $PWDPATH +} + +if [ ! -f "${STEPPATH}/config/ca.json" ]; then + init_if_possible +fi + +exec "${@}" \ No newline at end of file diff --git a/pkg/kubefate/constants.go b/pkg/kubefate/constants.go new file mode 100644 index 00000000..ef96959a --- /dev/null +++ b/pkg/kubefate/constants.go @@ -0,0 +1,1098 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubefate + +// GetDefaultYAML returns the default kubefate deployment yaml +func GetDefaultYAML() string { + return defaultKubeFATEYAMLVersion180 +} + +const defaultKubeFATEYAMLVersion180 = `apiVersion: v1 +kind: Namespace +metadata: + name: kube-fate + labels: + name: kube-fate +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubefate-admin + namespace: kube-fate +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kubefate +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubefate-role +subjects: + - kind: ServiceAccount + name: kubefate-admin + namespace: kube-fate +--- +apiVersion: v1 +kind: Secret +metadata: + name: kubefate-secret + namespace: kube-fate +type: Opaque +stringData: + kubefateUsername: {{.ServiceUserName}} + kubefatePassword: {{.ServicePassword}} + mariadbUsername: kubefate + mariadbPassword: kubefate +--- +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: kubefate-psp + namespace: kube-fate +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubefate-role + namespace: kube-fate +rules: +- apiGroups: + - "" + resources: + - namespaces + - configmaps + - services + - secrets + - persistentvolumeclaims + - serviceaccounts + verbs: + - get + - list + - create + - delete + - update + - patch +- apiGroups: + - "" + resources: + - pods + - pods/log + - nodes + verbs: + - get + - list +- apiGroups: + - apps + resources: + - deployments + - statefulsets + verbs: + - get + - list + - create + - delete + - update + - patch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - create + - delete + - update + - patch +- apiGroups: + - networking.istio.io + resources: + - gateway + - virtualservice + verbs: + - get + - create + - delete + - update + - patch +- apiGroups: + - policy + resources: + - podsecuritypolicies + verbs: + - get + - use + - create + - delete + - update + - patch +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + verbs: + - get + - create + - delete + - update + - patch +{{- if .UseImagePullSecrets}} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{.ImagePullSecretsName}} + namespace: kube-fate +data: + .dockerconfigjson: {{.RegistrySecretData}} +type: kubernetes.io/dockerconfigjson +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kubefate + namespace: kube-fate + labels: + fate: kubefate +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fate: kubefate + template: + metadata: + labels: + fate: kubefate + spec: + serviceAccountName: kubefate-admin + containers: + {{- if .UseRegistry}} + - image: {{.Registry}}/kubefate:v1.4.4 + {{- else }} + - image: federatedai/kubefate:v1.4.4 + {{- end }} + imagePullPolicy: IfNotPresent + name: kubefate + env: + - name: FATECLOUD_DB_TYPE + value: "mysql" + - name: FATECLOUD_DB_HOST + value: "mariadb" + - name: FATECLOUD_DB_PORT + value: "3306" + - name: FATECLOUD_DB_NAME + value: "kube_fate" + - name: FATECLOUD_DB_USERNAME + valueFrom: + secretKeyRef: + name: kubefate-secret + key: mariadbUsername + - name: FATECLOUD_DB_PASSWORD + valueFrom: + secretKeyRef: + name: kubefate-secret + key: mariadbPassword + - name: FATECLOUD_REPO_NAME + value: "kubefate" + - name: FATECLOUD_REPO_URL + value: "https://federatedai.github.io/KubeFATE" + - name: FATECLOUD_USER_USERNAME + valueFrom: + secretKeyRef: + name: kubefate-secret + key: kubefateUsername + - name: FATECLOUD_USER_PASSWORD + valueFrom: + secretKeyRef: + name: kubefate-secret + key: kubefatePassword + - name: FATECLOUD_SERVER_ADDRESS + value: "0.0.0.0" + - name: FATECLOUD_SERVER_PORT + value: "8080" + - name: FATECLOUD_LOG_LEVEL + value: "debug" + - name: FATECLOUD_LOG_NOCOLOR + value: "true" + ports: + - containerPort: 8080 + resources: + limits: + memory: "4Gi" + cpu: "2" + requests: + memory: 512Mi + cpu: "0.5" + {{- if .UseImagePullSecrets}} + imagePullSecrets: + - name: {{.ImagePullSecretsName}} + {{- end }} + restartPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mariadb + namespace: kube-fate + labels: + fate: mariadb +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fate: mariadb + template: + metadata: + labels: + fate: mariadb + spec: + serviceAccountName: kubefate-admin + containers: + {{- if .UseRegistry}} + - image: {{.Registry}}/mariadb:10 + {{- else }} + - image: mariadb:10 + {{- end }} + imagePullPolicy: IfNotPresent + name: mariadb + env: + - name: MYSQL_DATABASE + value: "kube_fate" + - name: MYSQL_ALLOW_EMPTY_PASSWORD + value: "1" + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: kubefate-secret + key: mariadbUsername + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: kubefate-secret + key: mariadbPassword + ports: + - containerPort: 3306 + resources: + limits: + memory: "4Gi" + cpu: "2" + requests: + memory: 512Mi + cpu: "0.5" + volumeMounts: + - name: mariadb-data + mountPath: /var/lib/mysql + restartPolicy: Always + {{- if .UseImagePullSecrets}} + imagePullSecrets: + - name: {{.ImagePullSecretsName}} + {{- end }} + volumes: + - name: mariadb-data + # If you want persistence, please modify it to the PV you want to use. + emptyDir: {} + # hostPath: + # path: /data/kubefate/mysql/db + # type: DirectoryOrCreate + # persistentVolumeClaim: + # claimName: "mariadb-data" +# --- +# apiVersion: v1 +# kind: PersistentVolumeClaim +# metadata: +# name: mariadb-data +# namespace: kube-fate +# spec: +# accessModes: +# - ReadWriteOnce +# storageClassName: +# resources: +# requests: +# storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: mariadb + namespace: kube-fate + labels: + fate: mariadb +spec: + ports: + - name: "3306" + port: 3306 + targetPort: 3306 + protocol: TCP + type: ClusterIP + selector: + fate: mariadb +--- +apiVersion: v1 +kind: Service +metadata: + name: kubefate + namespace: kube-fate + labels: + fate: kubefate +spec: + ports: + - name: "8080" + port: 8080 + targetPort: 8080 + protocol: TCP + type: ClusterIP + selector: + fate: kubefate +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kubefate + namespace: kube-fate +spec: + ingressClassName: nginx + rules: + - host: {{.Hostname}} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kubefate + port: + number: 8080` + +// GetDefaultIngressControllerYAML returns the default ingress controller deployment yaml +func GetDefaultIngressControllerYAML() string { + return defaultIngressNginxVersion111 +} + +const defaultIngressNginxVersion111 = ` +apiVersion: v1 +kind: Namespace +metadata: + name: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + +--- +# Source: ingress-nginx/templates/controller-serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +automountServiceAccountToken: true +--- +# Source: ingress-nginx/templates/controller-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +data: + allow-snippet-annotations: 'true' +--- +# Source: ingress-nginx/templates/clusterrole.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + name: ingress-nginx +rules: + - apiGroups: + - '' + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + - namespaces + verbs: + - list + - watch + - apiGroups: + - '' + resources: + - nodes + verbs: + - get + - apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch +--- +# Source: ingress-nginx/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + name: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx +subjects: + - kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/controller-role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +rules: + - apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - apiGroups: + - '' + resources: + - configmaps + - pods + - secrets + - endpoints + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - configmaps + resourceNames: + - ingress-controller-leader + verbs: + - get + - update + - apiGroups: + - '' + resources: + - configmaps + verbs: + - create + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch + - apiGroups: + - policy + resources: + - podsecuritypolicies + verbs: + - use +--- +# Source: ingress-nginx/templates/controller-rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx +subjects: + - kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/controller-service-webhook.yaml +apiVersion: v1 +kind: Service +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller-admission + namespace: ingress-nginx +spec: + type: ClusterIP + ports: + - name: https-webhook + port: 443 + targetPort: webhook + appProtocol: https + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller +--- +# Source: ingress-nginx/templates/controller-service.yaml +apiVersion: v1 +kind: Service +metadata: + annotations: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + type: {{.ServiceType}} + externalTrafficPolicy: Cluster + ipFamilyPolicy: SingleStack + ipFamilies: + - IPv4 + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + appProtocol: http + - name: https + port: 443 + protocol: TCP + targetPort: https + appProtocol: https + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller +--- +# Source: ingress-nginx/templates/controller-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + selector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller + revisionHistoryLimit: 10 + minReadySeconds: 0 + template: + metadata: + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller + spec: + dnsPolicy: ClusterFirst + containers: + - name: controller + image: k8s.gcr.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /wait-shutdown + args: + - /nginx-ingress-controller + - --watch-ingress-without-class=true + - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller + - --election-id=ingress-controller-leader + - --controller-class=k8s.io/ingress-nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + securityContext: + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + runAsUser: 101 + allowPrivilegeEscalation: true + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LD_PRELOAD + value: /usr/local/lib/libmimalloc.so + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: https + containerPort: 443 + protocol: TCP + - name: webhook + containerPort: 8443 + protocol: TCP + volumeMounts: + - name: webhook-cert + mountPath: /usr/local/certificates/ + readOnly: true + resources: + requests: + cpu: 100m + memory: 90Mi + nodeSelector: + kubernetes.io/os: linux + serviceAccountName: ingress-nginx + terminationGracePeriodSeconds: 300 + volumes: + - name: webhook-cert + secret: + secretName: ingress-nginx-admission +--- +# Source: ingress-nginx/templates/controller-ingressclass.yaml +# We don't support namespaced ingressClass yet +# So a ClusterRole and a ClusterRoleBinding is required +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: nginx + namespace: ingress-nginx +spec: + controller: k8s.io/ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/validating-webhook.yaml +# before changing this value, check the required kubernetes version +# https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#prerequisites +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + name: ingress-nginx-admission +webhooks: + - name: validate.nginx.ingress.kubernetes.io + matchPolicy: Equivalent + rules: + - apiGroups: + - networking.k8s.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - ingresses + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: + - v1 + clientConfig: + service: + namespace: ingress-nginx + name: ingress-nginx-controller-admission + path: /networking/v1/ingresses +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrole.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ingress-nginx-admission + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +rules: + - apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + verbs: + - get + - update +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ingress-nginx-admission + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx-admission +subjects: + - kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +rules: + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - create + - apiGroups: + - policy + resources: + - podsecuritypolicies + verbs: + - use +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx-admission +subjects: + - kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-createSecret.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: ingress-nginx-admission-create + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +spec: + template: + metadata: + name: ingress-nginx-admission-create + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + spec: + containers: + - name: create + image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 + imagePullPolicy: IfNotPresent + args: + - create + - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc + - --namespace=$(POD_NAMESPACE) + - --secret-name=ingress-nginx-admission + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + securityContext: + allowPrivilegeEscalation: false + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsNonRoot: true + runAsUser: 2000 +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-patchWebhook.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: ingress-nginx-admission-patch + namespace: ingress-nginx + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +spec: + template: + metadata: + name: ingress-nginx-admission-patch + labels: + helm.sh/chart: ingress-nginx-4.0.15 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.1.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + spec: + containers: + - name: patch + image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 + imagePullPolicy: IfNotPresent + args: + - patch + - --webhook-name=ingress-nginx-admission + - --namespace=$(POD_NAMESPACE) + - --patch-mutating=false + - --secret-name=ingress-nginx-admission + - --patch-failure-policy=Fail + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + securityContext: + allowPrivilegeEscalation: false + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsNonRoot: true + runAsUser: 2000` diff --git a/pkg/kubefate/kubefate.go b/pkg/kubefate/kubefate.go new file mode 100644 index 00000000..210f54d3 --- /dev/null +++ b/pkg/kubefate/kubefate.go @@ -0,0 +1,607 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubefate + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "mime/multipart" + "net/http" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/pkg/utils" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/modules" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "k8s.io/client-go/tools/portforward" + "sigs.k8s.io/yaml" +) + +// Client can be used to access KubeFATE service +type Client interface { + // CheckVersion returns the version of the KubeFATE service + CheckVersion() (string, error) + // EnsureChartExist makes sure the specified chart is downloaded and managed by kubefate + EnsureChartExist(name, version string, content []byte) error + // ListClusterByNamespace returns clusters list in the specified namespace + ListClusterByNamespace(namespace string) ([]*modules.Cluster, error) + + SubmitClusterInstallationJob(yamlStr string) (string, error) + SubmitClusterUpdateJob(yamlStr string) (string, error) + SubmitClusterDeletionJob(clusterUUID string) (string, error) + GetJobInfo(jobUUID string) (*modules.Job, error) + WaitJob(jobUUID string) (*modules.Job, error) + WaitClusterUUID(jobUUID string) (string, error) + StopJob(jobUUID string) error + + IngressAddress() string + IngressRuleHost() string +} + +type client struct { + apiVersion string + ingressRuleHost string + ingressAddress string + tls bool + username string + password string +} + +type pfClient struct { + client + fw *portforward.PortForwarder + stopChan chan struct{} +} + +// InstallationMeta describes the basic info of a KubeFATE installation +type InstallationMeta struct { + namespace string + kubefateDeployName string + kubefateIngressName string + clusterRoleName string + clusterRoleBindingName string + pspName string + yaml string + ingressControllerNamespace string + ingressControllerDeployName string + ingressControllerServiceName string + ingressControllerYAML string +} + +// ClusterArgs is the args to manage a cluster +type ClusterArgs struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + ChartName string `json:"chart_name"` + ChartVersion string `json:"chart_version"` + Cover bool `json:"cover"` + Data []byte `json:"data"` +} + +type ClusterJobResponse struct { + Data *modules.Job + Msg string +} + +type ClusterInfoResponse struct { + Data *modules.Cluster + Msg string +} + +type ChartListResponse struct { + Data []*modules.HelmChart + Msg string +} + +type ClusterListResponse struct { + Data []*modules.Cluster + Msg string +} + +// BuildInstallationMetaFromYAML builds a meta description of a KubeFATE installation +func BuildInstallationMetaFromYAML(yamlStr, ingressControllerYAMLStr string) (*InstallationMeta, error) { + // empty ingressControllerYAMLStr means we don't need to install ingress controller + if yamlStr == "" { + yamlStr = defaultKubeFATEYAMLVersion180 + } + // TODO: we should parse the passed yaml to determine these values, for now the hardcoded ones can work just fine + return &InstallationMeta{ + namespace: "kube-fate", + kubefateDeployName: "kubefate", + kubefateIngressName: "kubefate", + clusterRoleName: "kubefate-role", + clusterRoleBindingName: "kubefate", + pspName: "kubefate-psp", + yaml: yamlStr, + ingressControllerNamespace: "ingress-nginx", + ingressControllerDeployName: "ingress-nginx-controller", + ingressControllerServiceName: "ingress-nginx-controller", + ingressControllerYAML: ingressControllerYAMLStr, + }, nil +} + +func (c *client) IngressAddress() string { + return c.ingressAddress +} + +func (c *client) IngressRuleHost() string { + return c.ingressRuleHost +} + +func (c *client) CheckVersion() (string, error) { + url := c.getUrl("version") + body := bytes.NewReader(nil) + log.Info().Msgf("request info: %s", url) + request, err := http.NewRequest("GET", url, body) + if err != nil { + log.Err(err).Msgf("new request error") + return "", err + } + + token, err := c.getToken() + if err != nil { + return "", errors.Wrapf(err, "get token error") + } + authorization := fmt.Sprintf("Bearer %s", token) + + request.Header.Add("Authorization", authorization) + request.Host = c.ingressRuleHost + + resp, err := http.DefaultClient.Do(request) + if err != nil { + return "", errors.Wrapf(err, "http request error") + } + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "read resp body error") + } + log.Info().Msgf("request success, body: %s", string(respBody)) + + type VersionResultMsg struct { + Msg string + Version string + } + + versionResult := new(VersionResultMsg) + + err = json.Unmarshal(respBody, &versionResult) + if err != nil { + return "", errors.Wrapf(err, "Unmarshal resp body error") + } + + return versionResult.Version, nil +} + +func (c *client) EnsureChartExist(name, version string, content []byte) error { + resp, err := c.sendJSON("GET", "chart", nil) + if err != nil { + return err + } + body, err := c.parseResponse(resp) + if err != nil { + return err + } + var response ChartListResponse + if err := json.Unmarshal(body, &response); err != nil { + return err + } + for _, chart := range response.Data { + if chart.Name == name && chart.Version == version { + // just log but continue the uploading + log.Info().Msgf("chart %s:%s already exists, override", name, version) + break + } + } + _ = resp.Body.Close() + + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + fileWriter, err := bodyWriter.CreateFormFile("file", fmt.Sprintf("%s-%s.tgz", name, version)) + if err != nil { + return err + } + + _, err = fileWriter.Write(content) + if err != nil { + return errors.Wrapf(err, "failed to write content") + } + + contentType := bodyWriter.FormDataContentType() + log.Debug().Str("contentType", contentType).Msg("contentType") + _ = bodyWriter.Close() + + if err := utils.RetryWithMaxAttempts(func() error { + token, err := c.getToken() + if err != nil { + return errors.Wrapf(err, "get token error") + } + authorization := fmt.Sprintf("Bearer %s", token) + + urlStr := c.getUrl("chart") + payload := bodyBuf.Bytes() + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", contentType) + req.Header.Add("Authorization", authorization) + req.Host = c.ingressRuleHost + log.Info().Msg(fmt.Sprintf("Uploading file to %s", urlStr)) + resp, err = http.DefaultClient.Do(req) + return err + }, 3, 10*time.Second); err != nil { + return err + } + + _, err = c.parseResponse(resp) + return err +} + +func (c *client) ListClusterByNamespace(namespace string) ([]*modules.Cluster, error) { + resp, err := c.sendJSON("GET", "cluster", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + var response ClusterListResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + var res []*modules.Cluster + for _, cluster := range response.Data { + if cluster.NameSpace == namespace { + res = append(res, cluster) + break + } + } + return res, err +} + +func (c *client) getToken() (string, error) { + + login := map[string]string{ + "username": c.username, + "password": c.password, + } + + loginJsonB, err := json.Marshal(login) + + body := bytes.NewReader(loginJsonB) + loginUrl := c.getUrl("user/login") + + request, err := http.NewRequest("POST", loginUrl, body) + if err != nil { + return "", err + } + request.Host = c.ingressRuleHost + + var resp *http.Response + resp, err = http.DefaultClient.Do(request) + if err != nil { + return "", err + } + + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + result := map[string]interface{}{} + + err = json.Unmarshal(rbody, &result) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", errors.New(fmt.Sprint(result["message"])) + } + + token := fmt.Sprint(result["token"]) + + return token, nil +} + +func (c *client) SubmitClusterInstallationJob(yamlStr string) (string, error) { + body, err := c.buildClusterRequestBody(yamlStr) + if err != nil { + return "", err + } + resp, err := c.postJSON("cluster", body) + if err != nil { + return "", err + } + defer resp.Body.Close() + return c.getClusterJobUUID(resp) +} + +func (c *client) SubmitClusterUpdateJob(yamlStr string) (string, error) { + body, err := c.buildClusterRequestBody(yamlStr) + if err != nil { + return "", err + } + resp, err := c.putJSON("cluster", body) + if err != nil { + return "", err + } + defer resp.Body.Close() + return c.getClusterJobUUID(resp) +} + +func (c *client) SubmitClusterDeletionJob(clusterUUID string) (string, error) { + _, err := c.getClusterInfo(clusterUUID) + if err != nil { + if strings.ContainsAny(err.Error(), "record not found") { + log.Info().Msg("cluster record not found, skip deletion") + return "", nil + } + log.Error().Err(err).Msg("get cluster info error") + return "", err + } + resp, err := c.sendJSON("DELETE", "cluster/"+clusterUUID, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + + jobUUID, err := c.getClusterJobUUID(resp) + if err != nil { + if strings.ContainsAny(err.Error(), "record not found") { + log.Info().Msg("cluster record not found error, ignore") + return "", nil + } + } + return jobUUID, err +} + +func (c *client) getClusterInfo(clusterUUID string) (*modules.Cluster, error) { + resp, err := c.sendJSON("GET", "cluster/"+clusterUUID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + var response ClusterInfoResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + return response.Data, err +} + +func (c *client) GetJobInfo(jobUUID string) (*modules.Job, error) { + resp, err := c.sendJSON("GET", "job/"+jobUUID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + var response ClusterJobResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + return response.Data, err +} + +func (c *client) WaitJob(jobUUID string) (*modules.Job, error) { + for { + job, err := c.GetJobInfo(jobUUID) + if err != nil { + return nil, err + } + switch job.Status { + case modules.JobStatusRunning, modules.JobStatusPending: + time.Sleep(time.Second * 5) + continue + default: + log.Info().Msgf("job (%s) status: %v", job.Uuid, job.Status) + return job, nil + } + } +} + +func (c *client) StopJob(jobUUID string) error { + job, err := c.GetJobInfo(jobUUID) + if err != nil { + return err + } + if job.Status == modules.JobStatusRunning { + resp, err := c.sendJSON("PUT", "job/"+jobUUID+"?jobStatus=stop", nil) + if err != nil { + return errors.Wrapf(err, "failed to stop the running job: %s", jobUUID) + } + defer resp.Body.Close() + log.Info().Msg("Stop Job Success") + } + return nil +} + +func (c *client) WaitClusterUUID(jobUUID string) (string, error) { + for { + job, err := c.GetJobInfo(jobUUID) + if err != nil { + return "", err + } + if job.ClusterId != "" { + return job.ClusterId, nil + } + switch job.Status { + case modules.JobStatusFailed, modules.JobStatusTimeout, modules.JobStatusCanceled, modules.JobStatusStopping: + return "", errors.Errorf("failed to get ClusterUUID: job status: %s, job info: %v", job.Status.String(), job) + default: + log.Info().Msgf("waiting for ClusterUUID of job (%s) status: %v", job.Uuid, job.Status) + time.Sleep(time.Second * 5) + continue + } + } +} + +func (c *client) getClusterJobUUID(resp *http.Response) (string, error) { + bodyBytes, err := c.parseResponse(resp) + if err != nil { + return "", err + } + var response ClusterJobResponse + if err := json.Unmarshal(bodyBytes, &response); err != nil { + return "", err + } + return response.Data.Uuid, err +} + +func (c *client) buildClusterRequestBody(yamlStr string) (string, error) { + var m map[string]interface{} + err := yaml.Unmarshal([]byte(yamlStr), &m) + if err != nil { + return "", err + } + + name, ok := m["name"] + if !ok { + return "", errors.New("name not found") + } + + namespace, ok := m["namespace"] + if !ok { + return "", errors.New("namespace not found") + } + + chartVersion, ok := m["chartVersion"] + if !ok { + return "", errors.New("chartVersion not found") + } + + chartName, ok := m["chartName"] + if !ok { + chartName = "" + } + + var json = jsoniter.ConfigCompatibleWithStandardLibrary + valBJ, err := json.Marshal(m) + log.Info().Msg(fmt.Sprintf("Cluster operation request valBJ: %s", string(valBJ))) + if err != nil { + return "", err + } + + args := ClusterArgs{ + Name: name.(string), + Namespace: namespace.(string), + ChartName: chartName.(string), + ChartVersion: chartVersion.(string), + Cover: true, + Data: valBJ, + } + + body, err := json.Marshal(args) + if err != nil { + return "", err + } + return string(body), nil +} + +func (c *client) getUrl(path string) string { + address := c.ingressAddress + apiVersion := c.apiVersion + schemaStr := "http" + if c.tls { + schemaStr = "https" + } + return schemaStr + "://" + address + "/" + apiVersion + "/" + path +} + +func (c *client) sendJSON(method, path string, body interface{}) (*http.Response, error) { + var resp *http.Response + if err := utils.RetryWithMaxAttempts(func() error { + token, err := c.getToken() + if err != nil { + return errors.Wrapf(err, "get token error") + } + authorization := fmt.Sprintf("Bearer %s", token) + + urlStr := c.getUrl(path) + var payload []byte + if stringBody, ok := body.(string); ok { + payload = []byte(stringBody) + } else { + var err error + if payload, err = json.Marshal(body); err != nil { + return err + } + } + req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", authorization) + req.Host = c.ingressRuleHost + log.Info().Msg(fmt.Sprintf("%s request to %s with body %s", method, urlStr, string(payload))) + resp, err = http.DefaultClient.Do(req) + return err + }, 3, 10*time.Second); err != nil { + return nil, err + } + return resp, nil +} + +func (c *client) putJSON(path string, body interface{}) (*http.Response, error) { + return c.sendJSON("PUT", path, body) +} + +func (c *client) postJSON(path string, body interface{}) (*http.Response, error) { + return c.sendJSON("POST", path, body) +} + +func (c *client) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return body, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + m := make(map[string]string) + if err := json.Unmarshal(body, &m); err != nil { + log.Warn().Err(err).Msg("unable to unmarshal error body") + } else if errorMessage, ok := m["error"]; ok { + return body, errors.Errorf("request error: %s with error message: %s", response.Status, errorMessage) + } + return body, errors.Errorf("request error: %s with unspecified error message", response.Status) + } + return body, nil +} + +func (c *pfClient) Close() { + if c.stopChan != nil { + log.Info().Msgf("closing port forwarder %v", c.fw) + close(c.stopChan) + c.stopChan = nil + } +} diff --git a/pkg/kubefate/manager.go b/pkg/kubefate/manager.go new file mode 100644 index 00000000..3702fcbe --- /dev/null +++ b/pkg/kubefate/manager.go @@ -0,0 +1,479 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubefate + +import ( + "context" + "fmt" + "net/http" + "os" + "sort" + "time" + + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/pkg/utils" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + apiErr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + "k8s.io/kubectl/pkg/polymorphichelpers" + "k8s.io/kubectl/pkg/util/podutils" + "sigs.k8s.io/yaml" +) + +// ClientManager provides methods to work with KubeFATE as a client +type ClientManager interface { + // K8sClient returns the embedded K8s client + K8sClient() kubernetes.Client + // BuildClient retrieve the KubeFATE access info from the underlying K8s cluster and return a client instance + BuildClient() (Client, error) + // BuildPFClient build a KubeFATE client based on internal port-forwarding routine, caller must call the returned closerFn when no longer needs it + BuildPFClient() (client Client, closerFn func(), err error) +} + +// Manager extends ClientManager and provides methods to work with KubeFATE installation +type Manager interface { + ClientManager + // Install installs a KubeFATE deployment + Install(bool) error + // Uninstall uninstalls a KubeFATE installation + Uninstall() error + // InstallIngressNginxController installs a default ingress nginx controller + InstallIngressNginxController() error + // GetKubeFATEDeployment returns the Deployment object of KubeFATE + GetKubeFATEDeployment() (*appv1.Deployment, error) +} + +type manager struct { + client kubernetes.Client + meta *InstallationMeta +} + +// NewManager returns the KubeFATE manager +func NewManager(client kubernetes.Client, meta *InstallationMeta) Manager { + return &manager{ + client: client, + meta: meta, + } +} + +// NewClientManager returns the KubeFATE client manager +func NewClientManager(client kubernetes.Client, meta *InstallationMeta) ClientManager { + return &manager{ + client: client, + meta: meta, + } +} + +func (manager *manager) K8sClient() kubernetes.Client { + return manager.client +} + +func (manager *manager) Install(checkIngress bool) error { + kubefateDeployment, err := manager.GetKubeFATEDeployment() + if err == nil { + return errors.New("kubefate is already installed") + } + if err := manager.client.ApplyOrDeleteYAML(manager.meta.yaml, false); err != nil { + return errors.Wrapf(err, "failed to install kubefate, yaml: %s", manager.meta.yaml) + } + + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("checking kubefate deployment readiness...") + kubefateDeployment, err = manager.GetKubeFATEDeployment() + if err != nil { + log.Err(err).Msgf("kubefate deployment installation error") + return false + } + if kubefateDeployment.Status.ReadyReplicas > 0 { + return true + } + log.Info().Msgf("kubefate installation not ready yet, status: %s", kubefateDeployment.Status.String()) + return false + }, time.Minute*30, time.Second*10); err != nil { + return errors.Wrapf(err, "error checking kubefate deployment") + } + if checkIngress { + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("checking kubefate ingress readiness...") + ingress, err := manager.client.GetClientSet().NetworkingV1().Ingresses(manager.meta.namespace).Get(context.TODO(), manager.meta.kubefateIngressName, metav1.GetOptions{}) + if err != nil { + log.Err(err).Msgf("kubefate ingress installation error") + return false + } + if len(ingress.Status.LoadBalancer.Ingress) > 0 { + return true + } + log.Info().Msgf("kubefate ingress installation not ready yet, status: %s", ingress.Status.String()) + return false + }, time.Minute*30, time.Second*10); err != nil { + return errors.Wrapf(err, "error checking kubefate ingress deployment") + } + } + + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("verifying kubefate version...") + kfc, err := manager.BuildClient() + if err != nil { + var closer func() + kfc, closer, err = manager.BuildPFClient() + if closer != nil { + defer closer() + } + if err != nil { + log.Err(err).Msg("error getting kubefate client instance") + return false + } + } + if _, err := kfc.CheckVersion(); err != nil { + log.Err(err).Msg("error checking kubefate version") + return false + } + return true + }, time.Minute*20, time.Second*10); err != nil { + return errors.Wrapf(err, "error checking kubefate service readiness") + } + log.Info().Msg("kubefate is installed and ready") + return nil +} + +func (manager *manager) Uninstall() error { + clientSet := manager.client.GetClientSet() + if err := clientSet.RbacV1().ClusterRoleBindings().Delete(context.TODO(), manager.meta.clusterRoleBindingName, metav1.DeleteOptions{}); err != nil && !apiErr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete clusterrolebinding") + } + log.Info().Msgf("clusterrolebinding %s deleted", manager.meta.clusterRoleBindingName) + + if err := clientSet.RbacV1().ClusterRoles().Delete(context.TODO(), manager.meta.clusterRoleName, metav1.DeleteOptions{}); err != nil && !apiErr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete clusterrole") + } + log.Info().Msgf("clusterrole %s deleted", manager.meta.clusterRoleName) + + if err := clientSet.PolicyV1beta1().PodSecurityPolicies().Delete(context.TODO(), manager.meta.pspName, metav1.DeleteOptions{}); err != nil && !apiErr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete psp") + } + log.Info().Msgf("psp %s deleted", manager.meta.pspName) + + // TODO: explicitly delete other namespaced resources + + if err := clientSet.CoreV1().Namespaces().Delete(context.TODO(), manager.meta.namespace, metav1.DeleteOptions{}); err != nil && !apiErr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete namespace") + } + log.Info().Msgf("namespace %s deleted", manager.meta.namespace) + + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("checking namespace %s removing result...", manager.meta.namespace) + ns, err := clientSet.CoreV1().Namespaces().Get(context.TODO(), manager.meta.namespace, metav1.GetOptions{}) + if err != nil { + if apiErr.IsNotFound(err) { + return true + } + log.Warn().Err(err).Msgf("error getting namespace status") + return false + } + if ns != nil { + log.Info().Msgf("ns %s not deleted yet, status: %s", ns.Name, ns.Status.String()) + } + return false + }, time.Minute*30, time.Second*10); err != nil { + return errors.Wrapf(err, "error deleting namespace %s", manager.meta.namespace) + } + log.Info().Msg("kubefate deleted") + return nil +} + +func (manager *manager) BuildClient() (Client, error) { + kubefateDeployment, err := manager.GetKubeFATEDeployment() + if err != nil { + return nil, err + } + if kubefateDeployment.Status.ReadyReplicas == 0 { + return nil, errors.New("kubefate deployment is not ready") + } + envList := kubefateDeployment.Spec.Template.Spec.Containers[0].Env + client := &client{ + apiVersion: "v1", + } + if client.username = manager.getValueFromEnvs(envList, "FATECLOUD_USER_USERNAME"); client.username == "" { + log.Warn().Msgf("cannot get kubefate username for installation %v, using default", manager.meta) + client.username = "admin" + } + + if client.password = manager.getValueFromEnvs(envList, "FATECLOUD_USER_PASSWORD"); client.password == "" { + log.Warn().Msgf("cannot get kubefate password for installation %v, using default", manager.meta) + client.password = "admin" + } + + ingress, err := manager.client.GetClientSet().NetworkingV1().Ingresses(manager.meta.namespace).Get(context.TODO(), manager.meta.kubefateIngressName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get ingress info") + } + log.Debug().Msgf("ingress spec: %v, status: %v", ingress.Spec, ingress.Status) + if len(ingress.Spec.Rules) == 0 || len(ingress.Status.LoadBalancer.Ingress) == 0 { + return nil, errors.New("kubefate ingress is not available") + } + + client.ingressRuleHost = ingress.Spec.Rules[0].Host + if client.ingressAddress = ingress.Status.LoadBalancer.Ingress[0].Hostname; client.ingressAddress == "" { + client.ingressAddress = ingress.Status.LoadBalancer.Ingress[0].IP + } + if client.ingressAddress == "" { + return nil, errors.New("cannot get ingress access info") + } + client.tls = ingress.Spec.TLS != nil + + // try to use the node port address instead + if manager.meta.ingressControllerYAML != "" { + service, err := manager.client.GetClientSet().CoreV1().Services(manager.meta.ingressControllerNamespace).Get(context.TODO(), + manager.meta.ingressControllerServiceName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrapf(err, "failed to get ingress controller service") + } + if service.Spec.Type == corev1.ServiceTypeNodePort { + log.Info().Msg("using ingress service node port address") + client.ingressAddress, err = manager.getIngressControllerServiceNodePortAddress("http") + } + } + if _, err := client.CheckVersion(); err != nil { + return nil, errors.Wrapf(err, "cannot verify the client connection") + } + return client, nil +} + +func (manager *manager) BuildPFClient() (Client, func(), error) { + kubefateDeployment, err := manager.GetKubeFATEDeployment() + if err != nil { + return nil, nil, err + } + if kubefateDeployment.Status.ReadyReplicas == 0 { + return nil, nil, errors.New("kubefate deployment is not ready") + } + envList := kubefateDeployment.Spec.Template.Spec.Containers[0].Env + client := &pfClient{ + client: client{ + apiVersion: "v1", + }, + } + if client.username = manager.getValueFromEnvs(envList, "FATECLOUD_USER_USERNAME"); client.username == "" { + log.Warn().Msgf("cannot get kubefate username for installation %v, using default", manager.meta) + client.username = "admin" + } + + if client.password = manager.getValueFromEnvs(envList, "FATECLOUD_USER_PASSWORD"); client.password == "" { + log.Warn().Msgf("cannot get kubefate password for installation %v, using default", manager.meta) + client.password = "admin" + } + + fw, stopChan, err := manager.setUpPortForwarder() + if err != nil { + return nil, nil, errors.Wrapf(err, "fail to setup portt forwarder") + } + if err := utils.RetryWithMaxAttempts(func() error { + ports, err := fw.GetPorts() + if err != nil { + return err + } + client.ingressAddress = fmt.Sprintf("localhost:%d", ports[0].Local) + client.fw = fw + client.stopChan = stopChan + // we don't really care about the Host field when using port forwarder, but lets try our best to get the real one configured + client.ingressRuleHost = "kubefate.net" + ingress, err := manager.client.GetClientSet().NetworkingV1().Ingresses(manager.meta.namespace).Get(context.TODO(), manager.meta.kubefateIngressName, metav1.GetOptions{}) + if err == nil { + log.Debug().Msgf("ingress spec: %v, status: %v", ingress.Spec, ingress.Status) + if len(ingress.Spec.Rules) != 0 { + client.ingressRuleHost = ingress.Spec.Rules[0].Host + } + log.Warn().Msgf("kubefate ingress spec contain empty rules") + } else { + log.Err(err).Msgf("failed to query kubefate ingress info, using default one") + } + return nil + }, 10, 1*time.Second); err != nil { + return nil, nil, errors.Wrapf(err, "failed to setup port-forwarding for kubefate pod") + } + return client, client.Close, nil +} + +func (manager *manager) InstallIngressNginxController() error { + if manager.meta.ingressControllerYAML == "" { + return errors.Errorf("no ingress controller yaml provided") + } + if err := manager.client.ApplyOrDeleteYAML(manager.meta.ingressControllerYAML, true); err != nil { + return errors.Wrapf(err, "failed to clean-up ingress controller, yaml: %s", manager.meta.ingressControllerYAML) + } + + if err := utils.ExecuteWithTimeout(func() bool { + if err := manager.client.ApplyOrDeleteYAML(manager.meta.ingressControllerYAML, false); err != nil { + log.Err(err).Msgf("failed to install ingress controller, yaml: %s", manager.meta.ingressControllerYAML) + return false + } + return true + }, time.Minute*30, time.Second*20); err != nil { + return errors.Wrapf(err, "error installing ingress controller") + } + + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("checking ingress controller deployment readiness...") + controllerDeployment, err := manager.GetIngressNginxControllerDeployment() + if err != nil { + log.Err(err).Msgf("ingress controller deployment installation error") + return false + } + if controllerDeployment.Status.ReadyReplicas > 0 { + return true + } + log.Info().Msgf("ingress controller installation not ready yet, status: %s", controllerDeployment.Status.String()) + return false + }, time.Minute*30, time.Second*10); err != nil { + return errors.Wrapf(err, "error checking ingress controller deployment") + } + return nil +} + +func (manager *manager) getValueFromEnvs(envList []v1.EnvVar, key string) string { + for _, env := range envList { + if env.Name != key { + continue + } + if env.Value != "" { + return env.Value + } + if env.ValueFrom != nil { + value, err := manager.getSecretRefValue(env.ValueFrom) + if err != nil { + log.Err(err).Msgf("failed to gett secret value for ") + return "" + } + return value + } + return "" + } + return "" +} + +func (manager *manager) getSecretRefValue(source *v1.EnvVarSource) (string, error) { + secret, err := manager.client.GetClientSet().CoreV1().Secrets(manager.meta.namespace).Get(context.TODO(), source.SecretKeyRef.Name, metav1.GetOptions{}) + if err != nil { + return "", err + } + if data, ok := secret.Data[source.SecretKeyRef.Key]; ok { + return string(data), nil + } + return "", fmt.Errorf("key %s not found in secret %s", source.SecretKeyRef.Key, source.SecretKeyRef.Name) +} + +func (manager *manager) GetKubeFATEDeployment() (*appv1.Deployment, error) { + return manager.client.GetClientSet().AppsV1().Deployments(manager.meta.namespace).Get(context.TODO(), manager.meta.kubefateDeployName, metav1.GetOptions{}) +} + +func (manager *manager) GetIngressNginxControllerDeployment() (*appv1.Deployment, error) { + return manager.client.GetClientSet().AppsV1().Deployments(manager.meta.ingressControllerNamespace).Get(context.TODO(), manager.meta.ingressControllerDeployName, metav1.GetOptions{}) +} + +func (manager *manager) getIngressControllerServiceNodePortAddress(portName string) (address string, err error) { + host := "" + log.Info().Msgf("retrieving address for service: %s, port: %s in namespace: %s", manager.meta.ingressControllerServiceName, portName, manager.meta.ingressControllerNamespace) + service, err := manager.K8sClient().GetClientSet().CoreV1().Services(manager.meta.ingressControllerNamespace). + Get(context.TODO(), manager.meta.ingressControllerServiceName, metav1.GetOptions{}) + if err != nil { + return + } + + serviceYAML, _ := yaml.Marshal(service) + log.Debug().Msgf("service yaml: %s", serviceYAML) + + nodePort := 0 + for _, p := range service.Spec.Ports { + if p.Name == portName { + nodePort = int(p.NodePort) + } + } + + nl, _ := manager.K8sClient().GetClientSet().CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + node := nl.Items[0] + for _, addr := range node.Status.Addresses { + if addr.Type == corev1.NodeExternalIP { + host = addr.Address + break + } else if addr.Type == corev1.NodeInternalIP { + host = addr.Address + } + } + if host == "" { + err = errors.New("cannot find node address") + } + address = fmt.Sprintf("%s:%d", host, nodePort) + log.Info().Msgf("ingress address: %v", address) + return +} + +func (manager *manager) setUpPortForwarder() (fw *portforward.PortForwarder, stopChan chan struct{}, err error) { + deploy, err := manager.GetKubeFATEDeployment() + if err != nil { + return + } + + config, err := manager.client.GetConfig() + if err != nil { + return + } + + ns, selector, err := polymorphichelpers.SelectorsForObject(deploy) + if err != nil { + return + } + clientset, err := corev1client.NewForConfig(config) + if err != nil { + return + } + pod, _, err := polymorphichelpers.GetFirstPod(clientset, ns, selector.String(), time.Second*20, func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }) + if err != nil { + return + } + + req := manager.client.GetClientSet().CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()). + Resource("pods").Name(pod.GetName()).SubResource("portforward") + + roundTripper, upgrader, err := spdy.RoundTripperFor(config) + if err != nil { + return + } + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, req.URL()) + + // 8080 is kubefate's listening port + portMapping := []string{":8080"} + stopChan = make(chan struct{}, 1) + readyChannel := make(chan struct{}) + fw, err = portforward.New(dialer, portMapping, stopChan, readyChannel, os.Stdout, os.Stderr) + if err != nil { + return + } + + go func() { + err = fw.ForwardPorts() + if err != nil { + log.Err(err).Msgf("error forwarding ports for %v", fw) + } + log.Info().Msgf("shutdown port forwarder %v", fw) + }() + return +} diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go new file mode 100644 index 00000000..534ddad3 --- /dev/null +++ b/pkg/kubernetes/client.go @@ -0,0 +1,168 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + apierr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + "k8s.io/client-go/tools/clientcmd" +) + +// Client provides methods to interact with a Kubernetes API server +type Client interface { + // GetClientSet returns the ClientSet for future use + GetClientSet() kubernetes.Interface + // GetConfig returns the *rest.Config for future use + GetConfig() (*rest.Config, error) + // ApplyOrDeleteYAML applies or delete the yaml content to/from the target cluster + ApplyOrDeleteYAML(yamlStr string, delete bool) error +} + +// client contains necessary info and object to work with a kubernetes cluster +type client struct { + dynamicClient dynamic.Interface + clientSet kubernetes.Interface + config *rest.Config +} + +// NewKubernetesClient returns a client struct based on the kubeconfig path +func NewKubernetesClient(kubeconfigPath string, kubeconfigContent string) (Client, error) { + var config *rest.Config + var err error + if kubeconfigPath != "" { + log.Debug().Msg("build client with kubeconfigPath") + config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) + } else if kubeconfigContent != "" { + log.Debug().Msg("build client with kubeconfigContent") + config, err = clientcmd.RESTConfigFromKubeConfig([]byte(kubeconfigContent)) + } else { + err = errors.New("neither kubeconfigPath nor kubeconfigContent specified") + } + if err != nil { + return nil, err + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + return &client{ + dynamicClient: dynamicClient, + clientSet: clientSet, + config: config, + }, nil +} + +func (c *client) GetClientSet() kubernetes.Interface { + return c.clientSet +} + +func (c *client) GetConfig() (*rest.Config, error) { + return c.config, nil +} + +func (c *client) ApplyOrDeleteYAML(yamlStr string, delete bool) error { + groups, err := restmapper.GetAPIGroupResources(c.clientSet.Discovery()) + if err != nil { + return err + } + mapper := restmapper.NewDiscoveryRESTMapper(groups) + + reader := yaml.NewYAMLReader(bufio.NewReader(bytes.NewReader([]byte(yamlStr)))) + for { + docBytes, err := reader.Read() + if err != nil { + if err == io.EOF { + break + } + return errors.Wrapf(err, "failed to decode yaml content") + } + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{}, + } + + // Unmarshal the YAML document into the unstructured object. + if err := yaml.Unmarshal(docBytes, &obj.Object); err != nil { + return err + } + gvk := obj.GroupVersionKind() + + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + resourceStr := fmt.Sprintf("%s(%s)", obj.GetName(), mapping.GroupVersionKind.String()) + if delete { + log.Info().Msgf("Deleting %s with scope: %s", resourceStr, string(mapping.Scope.Name())) + } else { + log.Info().Msgf("Applying %s with scope: %s", resourceStr, string(mapping.Scope.Name())) + } + obj.SetManagedFields(nil) + var dri dynamic.ResourceInterface + if mapping.Scope.Name() == meta.RESTScopeNameNamespace { + if obj.GetNamespace() == "" { + log.Debug().Msg("using default namespace") + obj.SetNamespace("default") + } + dri = c.dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace()) + } else { + dri = c.dynamicClient.Resource(mapping.Resource) + } + force := true + if delete { + err := dri.Delete(context.TODO(), obj.GetName(), v1.DeleteOptions{}) + if err != nil { + if apierr.IsNotFound(err) { + log.Info().Msgf("Resource %s not existing, continue", resourceStr) + } else { + return errors.Wrapf(err, "failed to delete resource %s", resourceStr) + } + } else { + log.Info().Msgf("Done deleting %s with scope: %s", resourceStr, string(mapping.Scope.Name())) + } + } else { + resp, err := dri.Patch(context.TODO(), obj.GetName(), types.ApplyPatchType, docBytes, v1.PatchOptions{ + Force: &force, + FieldManager: "fed-lifecycle-manager", + }) + if err != nil { + return err + } + log.Info().Msgf("Done applying %s with scope: %s, UID: %s", resourceStr, string(mapping.Scope.Name()), string(resp.GetUID())) + } + } + return nil +} diff --git a/pkg/utils/retry.go b/pkg/utils/retry.go new file mode 100644 index 00000000..bf98a0ff --- /dev/null +++ b/pkg/utils/retry.go @@ -0,0 +1,59 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// ExecuteWithTimeout takes a function and invokes it, if the function returns false then it will +// retry it after sleeping interval seconds, until it returns true or timeout +func ExecuteWithTimeout(action func() bool, timeout time.Duration, interval time.Duration) error { + log.Info().Msgf("start for-loop with timeout %v, interval %v", timeout, interval) + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + for { + select { + case <-ctx.Done(): + return errors.New("operation timed out") + default: + if action() { + return nil + } + time.Sleep(interval) + } + } +} + +// RetryWithMaxAttempts keeps invoking the passed function until maximum attempts is reached or the +// function returns no error +func RetryWithMaxAttempts(action func() error, attempts int, interval time.Duration) error { + var err error + for i := 0; i < attempts; i++ { + if i > 0 { + log.Err(err).Msgf("retry (%v remaining) in %v seconds", attempts-i, interval) + time.Sleep(interval) + } + err = action() + if err == nil { + return nil + } + } + return errors.Wrapf(err, "failed after %v retries", attempts) +} diff --git a/rbac_config.yaml b/rbac_config.yaml new file mode 100644 index 00000000..a51d29dc --- /dev/null +++ b/rbac_config.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: fedlcm + labels: + app: fedlcm +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fedlcm-admin + namespace: fedlcm +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: fedlcm-binding + namespace: fedlcm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: fedlcm-role +subjects: + - kind: ServiceAccount + name: fedlcm-admin + namespace: fedlcm +--- +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: fedlcm-psp + namespace: fedlcm +spec: + privileged: false + seLinux: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + runAsUser: + rule: RunAsAny + fsGroup: + rule: RunAsAny + volumes: + - '*' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: fedlcm-role + namespace: fedlcm +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - fedlcm-psp \ No newline at end of file diff --git a/server/api/auth.go b/server/api/auth.go new file mode 100644 index 00000000..b2fbd672 --- /dev/null +++ b/server/api/auth.go @@ -0,0 +1,124 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "time" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/domain/repo" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "k8s.io/apimachinery/pkg/util/rand" +) + +const ( + idKey = "id" + nameKey = "name" + uuidKey = "uuid" + authErrorKey = "AUTH_ERROR" +) + +var authMiddleware *jwt.GinJWTMiddleware + +func getKey() string { + key := viper.GetString("lifecyclemanager.jwt.key") + if key == "" { + log.Warn().Msg("no pre-defined jwt key, generating a random one") + key = rand.String(32) + } + return key +} + +// CreateAuthMiddleware creates the authentication middleware +func CreateAuthMiddleware(repo repo.UserRepository) (err error) { + userApp := service.UserApp{UserRepo: repo} + authMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{ + Realm: "lifecycle manager jwt", + Key: []byte(getKey()), + Timeout: time.Hour * 24, + MaxRefresh: time.Hour, + IdentityKey: idKey, + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*service.PublicUser); ok { + return jwt.MapClaims{ + idKey: v.ID, + nameKey: v.Name, + uuidKey: v.UUID, + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + return &service.PublicUser{ + ID: uint(claims[idKey].(float64)), + Name: claims[nameKey].(string), + UUID: claims[uuidKey].(string), + } + }, + Authenticator: func(c *gin.Context) (interface{}, error) { + var loginInfo service.LoginInfo + if err := c.ShouldBindJSON(&loginInfo); err != nil { + return "", jwt.ErrMissingLoginValues + } + if user, err := userApp.Login(&loginInfo); err == nil { + log.Info().Msgf("user: %s logged in", loginInfo.Username) + return user, nil + } + return nil, jwt.ErrFailedAuthentication + }, + LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) { + c.JSON(code, GeneralResponse{ + Code: 0, + Message: "", + Data: token, + }) + }, + Authorizator: func(data interface{}, c *gin.Context) bool { + if v, ok := data.(*service.PublicUser); ok && v.Name == "Admin" { + return true + } + return false + }, + Unauthorized: func(c *gin.Context, code int, message string) { + c.JSON(code, GeneralResponse{ + Code: code, + Message: message, + Data: nil, + }) + }, + HTTPStatusMessageFunc: func(e error, c *gin.Context) string { + if e == jwt.ErrForbidden { + if v, exist := c.Get(authErrorKey); exist { + err := v.(error) + return err.Error() + } + } + return e.Error() + }, + TokenLookup: "header: Authorization, cookie: jwt", + TokenHeadName: "Bearer", + SendCookie: true, + SecureCookie: false, + CookieHTTPOnly: true, + CookieName: "jwt", + CookieSameSite: http.SameSiteDefaultMode, + }) + return +} diff --git a/server/api/certificate-authority.go b/server/api/certificate-authority.go new file mode 100644 index 00000000..ed0b6c63 --- /dev/null +++ b/server/api/certificate-authority.go @@ -0,0 +1,165 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/gin-gonic/gin" +) + +// CertificateAuthorityController provides API handlers for the certificate authority related APIs +type CertificateAuthorityController struct { + certificateAuthorityApp *service.CertificateAuthorityApp +} + +// NewCertificateAuthorityController returns a controller instance to handle certificate authority API requests +func NewCertificateAuthorityController(caRepo repo.CertificateAuthorityRepository) *CertificateAuthorityController { + return &CertificateAuthorityController{ + certificateAuthorityApp: &service.CertificateAuthorityApp{ + CertificateAuthorityRepo: caRepo, + }, + } +} + +// Route sets up route mappings to certificate-authority related APIs +func (controller *CertificateAuthorityController) Route(r *gin.RouterGroup) { + ca := r.Group("certificate-authority") + ca.Use(authMiddleware.MiddlewareFunc()) + { + ca.GET("", controller.get) + ca.POST("", controller.create) + ca.PUT("/:uuid", controller.update) + ca.GET("/built-in-ca", controller.getBuiltInCAConfig) + } +} + +// get returns the certificate authority info +// @Summary Return certificate authority info +// @Tags CertificateAuthority +// @Produce json +// @Success 200 {object} GeneralResponse{data=service.CertificateAuthorityDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate-authority [get] +func (controller *CertificateAuthorityController) get(c *gin.Context) { + if caInfo, err := controller.certificateAuthorityApp.Get(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: caInfo, + } + c.JSON(http.StatusOK, resp) + } +} + +// create a new certificate authority +// @Summary Create a new certificate authority +// @Tags CertificateAuthority +// @Produce json +// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The CA information, currently for the type field only '1(StepCA)' is supported" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate-authority [post] +func (controller *CertificateAuthorityController) create(c *gin.Context) { + if err := func() error { + caInfo := &service.CertificateAuthorityEditableItem{} + if err := c.ShouldBindJSON(caInfo); err != nil { + return err + } + return controller.certificateAuthorityApp.CreateCA(caInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// update the CA configuration +// @Summary Updates the certificate authority +// @Tags CertificateAuthority +// @Produce json +// @Param uuid path string true "certificate authority UUID" +// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The updated CA information" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate-authority/{uuid} [put] +func (controller *CertificateAuthorityController) update(c *gin.Context) { + if err := func() error { + uuid := c.Param("uuid") + caInfo := &service.CertificateAuthorityEditableItem{} + if err := c.ShouldBindJSON(caInfo); err != nil { + return err + } + return controller.certificateAuthorityApp.Update(uuid, caInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getBuiltInCAConfig returns the built-in certificate authority config +// @Summary Return the built-in certificate authority config +// @Tags CertificateAuthority +// @Produce json +// @Success 200 {object} GeneralResponse{data=entity.CertificateAuthorityConfigurationStepCA} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate-authority/built-in-ca [get] +func (controller *CertificateAuthorityController) getBuiltInCAConfig(c *gin.Context) { + caConfig, err := controller.certificateAuthorityApp.GetBuiltInCAConfig() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: caConfig, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/certificate.go b/server/api/certificate.go new file mode 100644 index 00000000..0f7dcaa7 --- /dev/null +++ b/server/api/certificate.go @@ -0,0 +1,111 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/gin-gonic/gin" +) + +// CertificateController provides API handlers for the certificate related APIs +type CertificateController struct { + certificateApp *service.CertificateApp +} + +// NewCertificateController returns a controller instance to handle certificate API requests +func NewCertificateController(caRepo repo.CertificateAuthorityRepository, + certRepo repo.CertificateRepository, + bindingRepo repo.CertificateBindingRepository, + participantFATERepo repo.ParticipantFATERepository, + participantOpenFLRepo repo.ParticipantOpenFLRepository, + federationFATERepo repo.FederationRepository, + federationOpenFLRepo repo.FederationRepository) *CertificateController { + return &CertificateController{ + certificateApp: &service.CertificateApp{ + CertificateAuthorityRepo: caRepo, + CertificateRepo: certRepo, + CertificateBindingRepo: bindingRepo, + ParticipantFATERepo: participantFATERepo, + ParticipantOpenFLRepo: participantOpenFLRepo, + FederationFATERepo: federationFATERepo, + FederationOpenFLRepo: federationOpenFLRepo, + }, + } +} + +// Route sets up route mappings to certificate related APIs +func (controller *CertificateController) Route(r *gin.RouterGroup) { + certificate := r.Group("certificate") + certificate.Use(authMiddleware.MiddlewareFunc()) + { + certificate.GET("", controller.list) + certificate.DELETE("/:uuid", controller.delete) + + } +} + +// list returns the certificate list +// @Summary Return issued certificate list +// @Tags Certificate +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.CertificateListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate [get] +func (controller *CertificateController) list(c *gin.Context) { + if certList, err := controller.certificateApp.List(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: certList, + } + c.JSON(http.StatusOK, resp) + } +} + +// delete removes the certificate which has no participant bindings +// @Summary Delete the certificate which has no participant bindings +// @Tags Certificate +// @Produce json +// @Param uuid path string true "Certificate UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /certificate/{uuid} [delete] +func (controller *CertificateController) delete(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.certificateApp.DeleteCertificate(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/chart.go b/server/api/chart.go new file mode 100644 index 00000000..4835e559 --- /dev/null +++ b/server/api/chart.go @@ -0,0 +1,110 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/gin-gonic/gin" +) + +// ChartController provides API handlers for the chart related APIs +type ChartController struct { + chartApp *service.ChartApp +} + +// NewChartController returns a controller instance to handle chart API requests +func NewChartController(chartRepo repo.ChartRepository) *ChartController { + return &ChartController{ + chartApp: &service.ChartApp{ + ChartRepo: chartRepo, + }, + } +} + +// Route sets up route mappings to chart related APIs +func (controller *ChartController) Route(r *gin.RouterGroup) { + chart := r.Group("chart") + chart.Use(authMiddleware.MiddlewareFunc()) + { + chart.GET("", controller.list) + chart.GET("/:uuid", controller.get) + // TODO: support add/delete + } +} + +// list returns the chart list +// @Summary Return chart list, optionally with the specified type +// @Tags Chart +// @Produce json +// @Param type query uint8 false "if set, it should be the chart type" +// @Success 200 {object} GeneralResponse{data=[]service.ChartListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /chart [get] +func (controller *ChartController) list(c *gin.Context) { + if charList, err := func() ([]service.ChartListItem, error) { + t64, err := strconv.ParseUint(c.DefaultQuery("type", "0"), 10, 8) + if err != nil { + return nil, err + } + t := entity.ChartType(t64) + return controller.chartApp.List(t) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: charList, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of a chart +// @Summary Get chart's detailed info +// @Tags Chart +// @Produce json +// @Param uuid path string true "Chart UUID" +// @Success 200 {object} GeneralResponse{data=service.ChartDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /chart/{uuid} [get] +func (controller *ChartController) get(c *gin.Context) { + uuid := c.Param("uuid") + if chartDetail, err := controller.chartApp.GetDetail(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: chartDetail, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/endpoint.go b/server/api/endpoint.go new file mode 100644 index 00000000..967a9fdd --- /dev/null +++ b/server/api/endpoint.go @@ -0,0 +1,305 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// EndpointController provides API handlers for the endpoint related APIs +type EndpointController struct { + endpointAppService *service.EndpointApp +} + +// NewEndpointController returns a controller instance to handle endpoint API requests +func NewEndpointController(infraProviderKubernetesRepo repo.InfraProviderRepository, + endpointKubeFATERepo repo.EndpointRepository, + participantFATERepo repo.ParticipantFATERepository, + participantOpenFLRepo repo.ParticipantOpenFLRepository, + eventRepo repo.EventRepository) *EndpointController { + return &EndpointController{ + endpointAppService: &service.EndpointApp{ + InfraProviderKubernetesRepo: infraProviderKubernetesRepo, + EndpointKubeFAETRepo: endpointKubeFATERepo, + ParticipantFATERepo: participantFATERepo, + ParticipantOpenFLRepo: participantOpenFLRepo, + EventRepo: eventRepo, + }, + } +} + +// Route sets up route mappings to endpoint related APIs +func (controller *EndpointController) Route(r *gin.RouterGroup) { + endpoint := r.Group("endpoint") + endpoint.Use(authMiddleware.MiddlewareFunc()) + { + endpoint.GET("", controller.list) + endpoint.GET("/:uuid", controller.get) + endpoint.GET("/kubefate/yaml", controller.getKubeFATEDeploymentYAML) + + endpoint.DELETE("/:uuid", controller.delete) + + endpoint.POST("/scan", controller.scan) + endpoint.POST("/:uuid/kubefate/check", controller.checkKubeFATE) + + endpoint.POST("", controller.create) + } +} + +// list returns the endpoints list +// @Summary Return endpoints list data +// @Tags Endpoint +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint [get] +func (controller *EndpointController) list(c *gin.Context) { + endpointList, err := controller.endpointAppService.GetEndpointList() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: endpointList, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of an endpoint +// @Summary Get endpoint's detailed info +// @Tags Endpoint +// @Produce json +// @Param uuid path string true "Endpoint UUID" +// @Success 200 {object} GeneralResponse{data=service.EndpointDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint/{uuid} [get] +func (controller *EndpointController) get(c *gin.Context) { + uuid := c.Param("uuid") + if endpointDetail, err := controller.endpointAppService.GetEndpointDetail(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: endpointDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// delete removes the endpoint +// @Summary Delete the endpoint +// @Tags Endpoint +// @Produce json +// @Param uuid path string true "Endpoint UUID" +// @Param uninstall query bool false "if set to true, the endpoint installation will be removed too" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint/{uuid} [delete] +func (controller *EndpointController) delete(c *gin.Context) { + uuid := c.Param("uuid") + uninstall, err := strconv.ParseBool(c.DefaultQuery("uninstall", "false")) + if err != nil { + uninstall = false + } + if err := controller.endpointAppService.DeleteEndpoint(uuid, uninstall); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// scan finds the endpoint installation status of an infra provider +// @Summary Scan the endpoints in an infra provider +// @Tags Endpoint +// @Produce json +// @Param provider body service.EndpointScanRequest true "Provider UUID and endpoint type" +// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint/scan [post] +func (controller *EndpointController) scan(c *gin.Context) { + if endpointDetail, err := func() ([]service.EndpointScanItem, error) { + request := &service.EndpointScanRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return nil, errors.Wrapf(err, "invalid request") + } + return controller.endpointAppService.ScanEndpoint(request) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: endpointDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// checkKubeFATE test connection to a KubeFATE endpoint +// @Summary Test connection to KubeFATE endpoint +// @Tags Endpoint +// @Produce json +// @Param uuid path string true "Endpoint UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint/{uuid}/kubefate/check [post] +func (controller *EndpointController) checkKubeFATE(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.endpointAppService.CheckKubeFATEConnection(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getKubeFATEDeploymentYAML returns the yaml content for deploying KubeFATE +// @Summary Get KubeFATE installation YAML content +// @Tags Endpoint +// @Produce json +// @Param service_username query string true "username of the created KubeFATE service" +// @Param service_password query string true "password of the created KubeFATE service" +// @Param hostname query string true "hostname domain name for the KubeFATE ingress object" +// @Param use_registry query bool true "use_registry is to choose to use registry or not" +// @Param registry query string true "registry is registry address" +// @Param use_registry_secret query bool true "use_registry_secret is to choose to use registry secret or not" +// @Param registry_server_url query string true "registry_server_url is registry's server url" +// @Param registry_username query string true "registry_username is registry's username" +// @Param registry_password query string true "registry_password is registry's password" +// @Success 200 {object} GeneralResponse{data=string} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint/kubefate/yaml [get] +func (controller *EndpointController) getKubeFATEDeploymentYAML(c *gin.Context) { + if yaml, err := func() (string, error) { + serviceUsername := c.DefaultQuery("service_username", "admin") + servicePassword := c.DefaultQuery("service_password", "admin") + hostname := c.DefaultQuery("hostname", "kubefate.net") + if serviceUsername == "" || servicePassword == "" || hostname == "" { + return "", errors.New("missing necessary parameters") + } + useRegistry, err := strconv.ParseBool(c.DefaultQuery("use_registry", "false")) + if err != nil { + return "", err + } + registry := c.DefaultQuery("registry", "") + if useRegistry && registry == "" { + return "", errors.New("missing registry") + } + useRegistrySecret, err := strconv.ParseBool(c.DefaultQuery("use_registry_secret", "false")) + if err != nil { + return "", err + } + registryServerURL := c.DefaultQuery("registry_server_url", "") + registryUsername := c.DefaultQuery("registry_username", "") + registryPassword := c.DefaultQuery("registry_password", "") + if useRegistrySecret && (registryServerURL == "" || registryUsername == "" || registryPassword == "") { + return "", errors.New("missing registry secret credentials") + } + return controller.endpointAppService.GetKubeFATEDeploymentYAML(serviceUsername, servicePassword, hostname, valueobject.KubeRegistryConfig{ + UseRegistry: useRegistry, + Registry: registry, + UseRegistrySecret: useRegistrySecret, + RegistrySecretConfig: valueobject.KubeRegistrySecretConfig{ + ServerURL: registryServerURL, + Username: registryUsername, + Password: registryPassword, + }, + }) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: yaml, + } + c.JSON(http.StatusOK, resp) + } +} + +// create a new endpoint +// @Summary Create a new endpoint by install a new one or add an existing one +// @Tags Endpoint +// @Produce json +// @Param provider body service.EndpointCreationRequest true "The endpoint information, currently for the type field only 'KubeFATE' is supported" +// @Success 200 {object} GeneralResponse "Success, the returned data contains the created endpoint" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /endpoint [post] +func (controller *EndpointController) create(c *gin.Context) { + if uuid, err := func() (string, error) { + req := &service.EndpointCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + return controller.endpointAppService.CreateEndpoint(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/event.go b/server/api/event.go new file mode 100644 index 00000000..c94abe19 --- /dev/null +++ b/server/api/event.go @@ -0,0 +1,75 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/gin-gonic/gin" +) + +// EventController provides API handlers for the event related APIs +type EventController struct { + EventApp *service.EventApp +} + +// NewEventController returns a controller instance to handle event API requests +func NewEventController(eventRepo repo.EventRepository) *EventController { + return &EventController{ + EventApp: &service.EventApp{ + EventRepo: eventRepo, + }, + } +} + +// Route sets up route mappings to event related APIs +func (controller *EventController) Route(r *gin.RouterGroup) { + event := r.Group("event") + event.Use(authMiddleware.MiddlewareFunc()) + { + event.GET("/:entity_uuid", controller.get) + } +} + +// list returns the event list of related entity +// @Summary Return event list of related entity +// @Tags Event +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.EventListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /event/{entity_uuid} [get] +func (controller *EventController) get(c *gin.Context) { + entity_uuid := c.Param("entity_uuid") + eventList, err := controller.EventApp.GetEventList(entity_uuid) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: eventList, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/federation.go b/server/api/federation.go new file mode 100644 index 00000000..ac166cfa --- /dev/null +++ b/server/api/federation.go @@ -0,0 +1,726 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + domainService "github.com/FederatedAI/FedLCM/server/domain/service" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// FederationController handles federation related APIs +type FederationController struct { + federationApp *service.FederationApp + participantAppService *service.ParticipantApp +} + +// NewFederationController returns a controller instance to handle federation API requests +func NewFederationController(infraProviderKubernetesRepo repo.InfraProviderRepository, + endpointKubeFATERepo repo.EndpointRepository, + federationFATERepo repo.FederationRepository, + federationOpenFLRepo repo.FederationRepository, + chartRepo repo.ChartRepository, + participantFATERepo repo.ParticipantFATERepository, + participantOpenflRepo repo.ParticipantOpenFLRepository, + certificateAuthorityRepo repo.CertificateAuthorityRepository, + certificateRepo repo.CertificateRepository, + certificateBindingRepo repo.CertificateBindingRepository, + registrationTokenOpenFLRepo repo.RegistrationTokenRepository, + eventRepo repo.EventRepository) *FederationController { + return &FederationController{ + federationApp: &service.FederationApp{ + FederationFATERepo: federationFATERepo, + ParticipantFATERepo: participantFATERepo, + FederationOpenFLRepo: federationOpenFLRepo, + ParticipantOpenFLRepo: participantOpenflRepo, + RegistrationTokenOpenFLRepo: registrationTokenOpenFLRepo, + }, + participantAppService: &service.ParticipantApp{ + ParticipantFATERepo: participantFATERepo, + FederationFATERepo: federationFATERepo, + FederationOpenFLRepo: federationOpenFLRepo, + ParticipantOpenFLRepo: participantOpenflRepo, + RegistrationTokenOpenFLRepo: registrationTokenOpenFLRepo, + EndpointKubeFATERepo: endpointKubeFATERepo, + InfraProviderKubernetesRepo: infraProviderKubernetesRepo, + ChartRepo: chartRepo, + CertificateAuthorityRepo: certificateAuthorityRepo, + CertificateRepo: certificateRepo, + CertificateBindingRepo: certificateBindingRepo, + EventRepo: eventRepo, + }, + } +} + +// Route sets up route mappings to federation related APIs +func (controller *FederationController) Route(r *gin.RouterGroup) { + federation := r.Group("federation") + + // we use the token string in the request for authentication + federation.POST("/openfl/envoy/register", controller.registerOpenFLEnvoy) + federation.GET("/openfl/envoy/:uuid", controller.getOpenFLEnvoyWithToken) + + federation.Use(authMiddleware.MiddlewareFunc()) + { + federation.GET("", controller.list) + } + fate := federation.Group("fate") + { + fate.POST("", controller.createFATE) + fate.GET("/:uuid", controller.getFATE) + fate.DELETE("/:uuid", controller.deleteFATE) + + fate.GET("/exchange/yaml", controller.getFATEExchangeDeploymentYAML) + fate.GET("/cluster/yaml", controller.getFATEClusterDeploymentYAML) + + fate.POST("/:uuid/exchange", controller.createFATEExchange) + fate.POST("/:uuid/exchange/external", controller.createExternalFATEExchange) + fate.POST("/:uuid/cluster", controller.createFATECluster) + fate.POST("/:uuid/partyID/check", controller.checkFATEPartyID) + fate.POST("/:uuid/cluster/external", controller.createExternalFATECluster) + + fate.DELETE("/:uuid/exchange/:exchangeUUID", controller.deleteFATEExchange) + fate.DELETE("/:uuid/cluster/:clusterUUID", controller.deleteFATECluster) + + fate.GET("/:uuid/participant", controller.getFATEParticipant) + + fate.GET("/:uuid/exchange/:exchangeUUID", controller.getFATEExchange) + fate.GET("/:uuid/cluster/:clusterUUID", controller.getFATECluster) + } + + openfl := federation.Group("openfl") + { + openfl.POST("", controller.createOpenFL) + openfl.GET("/:uuid", controller.getOpenFL) + openfl.DELETE("/:uuid", controller.deleteOpenFL) + + token := openfl.Group("/:uuid/token") + token.POST("", controller.createOpenFLToken) + token.GET("", controller.listOpenFLToken) + token.DELETE("/:tokenUUID", controller.deleteOpenFLToken) + + openfl.GET("/:uuid/participant", controller.getOpenFLParticipant) + + openfl.GET("/director/yaml", controller.getOpenFLDirectorDeploymentYAML) + + openfl.POST("/:uuid/director", controller.createOpenFLDirector) + openfl.DELETE("/:uuid/director/:directorUUID", controller.deleteOpenFLDirector) + openfl.GET("/:uuid/director/:directorUUID", controller.getOpenFLDirector) + + openfl.GET("/:uuid/envoy/:envoyUUID", controller.getOpenFLEnvoy) + openfl.DELETE("/:uuid/envoy/:envoyUUID", controller.deleteOpenFLEnvoy) + } +} + +// list returns the federation list +// @Summary Return federation list, +// @Tags Federation +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.FederationListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation [get] +func (controller *FederationController) list(c *gin.Context) { + if federationList, err := func() ([]service.FederationListItem, error) { + return controller.federationApp.List() + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: federationList, + } + c.JSON(http.StatusOK, resp) + } +} + +// createFATE creates a new FATE federation +// @Summary Create a new FATE federation +// @Tags Federation +// @Produce json +// @Param federation body service.FederationFATECreationRequest true "The federation info" +// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate [post] +func (controller *FederationController) createFATE(c *gin.Context) { + if uuid, err := func() (string, error) { + creationInfo := &service.FederationFATECreationRequest{} + if err := c.ShouldBindJSON(creationInfo); err != nil { + return "", err + } + return controller.federationApp.CreateFATEFederation(creationInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATE returns detailed information of a FATE federation +// @Summary Get specific info of a FATE federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.FederationFATEDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid} [get] +func (controller *FederationController) getFATE(c *gin.Context) { + uuid := c.Param("uuid") + if info, err := controller.federationApp.GetFATEFederation(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: info, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteFATE deletes the specified federation +// @Summary Delete a FATE federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid} [delete] +func (controller *FederationController) deleteFATE(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.federationApp.DeleteFATEFederation(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATEExchangeDeploymentYAML returns deployment yaml content for deploying FATE exchange +// @Summary Get FATE exchange deployment yaml +// @Tags Federation +// @Produce json +// @Param chart_uuid query string true "the chart uuid" +// @Param name query string true "name of the deployment" +// @Param namespace query string true "namespace of the deployment" +// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort" +// @Param registry query string true "FATE registry config saved in the infra provider" +// @Param use_registry query bool true "choose if use the customized registry config" +// @Param use_registry_secret query bool true "choose if use the customized registry secret" +// @Param enable_psp query bool true "choose if enable the podSecurityPolicy" +// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/exchange/yaml [get] +func (controller *FederationController) getFATEExchangeDeploymentYAML(c *gin.Context) { + if yaml, err := func() (string, error) { + chartUUID := c.DefaultQuery("chart_uuid", "") + name := c.DefaultQuery("name", "") + namespace := c.DefaultQuery("namespace", "") + if chartUUID == "" || name == "" || namespace == "" { + return "", errors.New("missing necessary parameters") + } + serviceType, err := strconv.Atoi(c.DefaultQuery("service_type", "1")) + if err != nil { + return "", errors.New("invalid service type parameter") + } + useRegistry, err := strconv.ParseBool(c.DefaultQuery("use_registry", "false")) + if err != nil { + return "", err + } + registry := c.DefaultQuery("registry", "") + if useRegistry && registry == "" { + return "", errors.New("missing registry") + } + useRegistrySecretFATE, err := strconv.ParseBool(c.DefaultQuery("use_registry_secret", "false")) + if err != nil { + return "", err + } + enablePSP, err := strconv.ParseBool(c.DefaultQuery("enable_psp", "true")) + if err != nil { + return "", err + } + return controller.participantAppService.GetFATEExchangeDeploymentYAML(&domainService.ParticipantFATEExchangeYAMLCreationRequest{ + ChartUUID: chartUUID, + Name: name, + Namespace: namespace, + ServiceType: entity.ParticipantDefaultServiceType(serviceType), + RegistryConfig: valueobject.KubeRegistryConfig{ + UseRegistry: useRegistry, + Registry: registry, + UseRegistrySecret: useRegistrySecretFATE, + }, + EnablePSP: enablePSP, + }) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: yaml, + } + c.JSON(http.StatusOK, resp) + } +} + +// createFATEExchange creates a new FATE exchange +// @Summary Create a new FATE exchange +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param creationRequest body service.ParticipantFATEExchangeCreationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/:uuid/exchange [post] +func (controller *FederationController) createFATEExchange(c *gin.Context) { + if exchangeUUID, err := func() (string, error) { + federationUUID := c.Param("uuid") + req := &domainService.ParticipantFATEExchangeCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + req.FederationUUID = federationUUID + return controller.participantAppService.CreateFATEExchange(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: exchangeUUID, + } + c.JSON(http.StatusOK, resp) + } +} + +// createExternalFATEExchange creates an external FATE exchange +// @Summary Create an external FATE exchange +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param creationRequest body domainService.ParticipantFATEExternalExchangeCreationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/:uuid/exchange/external [post] +func (controller *FederationController) createExternalFATEExchange(c *gin.Context) { + if exchangeUUID, err := func() (string, error) { + federationUUID := c.Param("uuid") + req := &domainService.ParticipantFATEExternalExchangeCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + req.FederationUUID = federationUUID + return controller.participantAppService.CreateExternalFATEExchange(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: exchangeUUID, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATEParticipant returns participant list of the specified federation +// @Summary Get participant list of the specified federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.ParticipantFATEListInFederation} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid}/participant [get] +func (controller *FederationController) getFATEParticipant(c *gin.Context) { + if participants, err := func() (*service.ParticipantFATEListInFederation, error) { + federationUUID := c.Param("uuid") + return controller.participantAppService.GetFATEParticipantList(federationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: participants, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteFATEExchange deletes the specified FATE exchange +// @Summary Delete a FATE exchange +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param exchangeUUID path string true "exchange UUID" +// @Param force query bool false "if set to true, will try to remove the exchange forcefully" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [delete] +func (controller *FederationController) deleteFATEExchange(c *gin.Context) { + exchangeUUID := c.Param("exchangeUUID") + if err := func() error { + force, err := strconv.ParseBool(c.DefaultQuery("force", "false")) + if err != nil { + return err + } + return controller.participantAppService.RemoveFATEExchange(exchangeUUID, force) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATEClusterDeploymentYAML returns deployment yaml content for deploying FATE cluster +// @Summary Get FATE cluster deployment yaml +// @Tags Federation +// @Produce json +// @Param chart_uuid query string true "the chart uuid" +// @Param federation_uuid query string true "the federation uuid" +// @Param party_id query int true "party id" +// @Param name query string true "name of the deployment" +// @Param namespace query string true "namespace of the deployment" +// @Param service_type query int true "type of the service to be exposed" +// @Param use_registry query bool true "choose if use the FATE registry config saved in the infra provider" +// @Param use_registry_secret query bool true "choose if use the FATE registry secret saved in the infra provider" +// @Param registry query string true "FATE registry config saved in the infra provider" +// @Param enable_persistence query bool true "choose if use the persistent volume" +// @Param storage_class query string true "provide the name of StorageClass" +// @Param enable_psp query bool true "choose if enable the podSecurityPolicy" +// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/cluster/yaml [get] +func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Context) { + if yaml, err := func() (string, error) { + chartUUID := c.DefaultQuery("chart_uuid", "") + federationUUID := c.DefaultQuery("federation_uuid", "") + name := c.DefaultQuery("name", "") + namespace := c.DefaultQuery("namespace", "") + if chartUUID == "" || name == "" || namespace == "" || federationUUID == "" { + return "", errors.New("missing necessary parameters") + } + partyID, err := strconv.Atoi(c.Query("party_id")) + if err != nil { + return "", err + } + serviceType, err := strconv.Atoi(c.DefaultQuery("service_type", "1")) + if err != nil { + return "", errors.New("invalid service type parameter") + } + useRegistry, err := strconv.ParseBool(c.DefaultQuery("use_registry", "false")) + if err != nil { + return "", err + } + registry := c.DefaultQuery("registry", "") + if useRegistry && registry == "" { + return "", errors.New("missing registry") + } + useRegistrySecret, err := strconv.ParseBool(c.DefaultQuery("use_registry_secret", "false")) + if err != nil { + return "", err + } + enablePersistence, err := strconv.ParseBool(c.DefaultQuery("enable_persistence", "false")) + if err != nil { + return "", err + } + storageClass := c.DefaultQuery("storage_class", "") + if enablePersistence && storageClass == "" { + return "", errors.New("missing storage class name") + } + if err != nil { + return "", err + } + + enablePSP, err := strconv.ParseBool(c.DefaultQuery("enable_psp", "true")) + if err != nil { + return "", err + } + return controller.participantAppService.GetFATEClusterDeploymentYAML(&domainService.ParticipantFATEClusterYAMLCreationRequest{ + ParticipantFATEExchangeYAMLCreationRequest: domainService.ParticipantFATEExchangeYAMLCreationRequest{ + ChartUUID: chartUUID, + Name: name, + Namespace: namespace, + ServiceType: entity.ParticipantDefaultServiceType(serviceType), + RegistryConfig: valueobject.KubeRegistryConfig{ + UseRegistry: useRegistry, + Registry: registry, + UseRegistrySecret: useRegistrySecret, + }, + EnablePSP: enablePSP, + }, + FederationUUID: federationUUID, + PartyID: partyID, + EnablePersistence: enablePersistence, + StorageClass: storageClass, + }) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: yaml, + } + c.JSON(http.StatusOK, resp) + } +} + +// createFATECluster creates a new FATE cluster +// @Summary Create a new FATE cluster +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param creationRequest body service.ParticipantFATEClusterCreationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/:uuid/cluster [post] +func (controller *FederationController) createFATECluster(c *gin.Context) { + if exchangeUUID, err := func() (string, error) { + federationUUID := c.Param("uuid") + req := &domainService.ParticipantFATEClusterCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + req.FederationUUID = federationUUID + return controller.participantAppService.CreateFATECluster(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: exchangeUUID, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteFATECluster deletes the specified FATE cluster +// @Summary Delete a FATE cluster +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param clusterUUID path string true "cluster UUID" +// @Param force query bool false "if set to true, will try to remove the cluster forcefully" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [delete] +func (controller *FederationController) deleteFATECluster(c *gin.Context) { + clusterUUID := c.Param("clusterUUID") + if err := func() error { + force, err := strconv.ParseBool(c.DefaultQuery("force", "false")) + if err != nil { + return err + } + return controller.participantAppService.RemoveFATECluster(clusterUUID, force) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// createExternalFATECluster creates an external FATE cluster +// @Summary Create an external FATE cluster +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param creationRequest body service.ParticipantFATEExternalClusterCreationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/:uuid/cluster/external [post] +func (controller *FederationController) createExternalFATECluster(c *gin.Context) { + if clusterUUID, err := func() (string, error) { + federationUUID := c.Param("uuid") + req := &domainService.ParticipantFATEExternalClusterCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + req.FederationUUID = federationUUID + return controller.participantAppService.CreateExternalFATECluster(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: clusterUUID, + } + c.JSON(http.StatusOK, resp) + } +} + +// checkFATEPartyID checks if the party ID is available +// @Summary Check if the party ID is available +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param party_id query int true "party ID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/:uuid/partyID/check [post] +func (controller *FederationController) checkFATEPartyID(c *gin.Context) { + if err := func() error { + federationUUID := c.Param("uuid") + partyID, err := strconv.Atoi(c.Query("party_id")) + if err != nil { + return err + } + return controller.participantAppService.CheckFATPartyID(federationUUID, partyID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATEExchange returns detailed information of a FATE exchange +// @Summary Get specific info of FATE Exchange +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.FATEExchangeDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [get] +func (controller *FederationController) getFATEExchange(c *gin.Context) { + exchangeUUID := c.Param("exchangeUUID") + if exchangeDetail, err := controller.participantAppService.GetFATEExchangeDetail(exchangeUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: exchangeDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// getFATECluster returns detailed information of a FATE cluster +// @Summary Get specific info of FATE cluster +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.FATEClusterDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [get] +func (controller *FederationController) getFATECluster(c *gin.Context) { + clusterUUID := c.Param("clusterUUID") + if clusterDetail, err := controller.participantAppService.GetFATEClusterDetail(clusterUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: clusterDetail, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/federation_openfl.go b/server/api/federation_openfl.go new file mode 100644 index 00000000..b7262e32 --- /dev/null +++ b/server/api/federation_openfl.go @@ -0,0 +1,479 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + domainService "github.com/FederatedAI/FedLCM/server/domain/service" + "github.com/gin-gonic/gin" +) + +// createOpenFL creates a new OpenFL federation +// @Summary Create a new OpenFL federation +// @Tags Federation +// @Produce json +// @Param federation body service.FederationOpenFLCreationRequest true "The federation info" +// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl [post] +func (controller *FederationController) createOpenFL(c *gin.Context) { + if uuid, err := func() (string, error) { + creationInfo := &service.FederationOpenFLCreationRequest{} + if err := c.ShouldBindJSON(creationInfo); err != nil { + return "", err + } + return controller.federationApp.CreateOpenFLFederation(creationInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFL returns detailed information of an OpenFL federation +// @Summary Get specific info of an OpenFL federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.FederationOpenFLDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid} [get] +func (controller *FederationController) getOpenFL(c *gin.Context) { + uuid := c.Param("uuid") + if info, err := controller.federationApp.GetOpenFLFederation(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: info, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteOpenFL deletes the specified federation +// @Summary Delete an OpenFL federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid} [delete] +func (controller *FederationController) deleteOpenFL(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.federationApp.DeleteOpenFLFederation(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// listOpenFLToken returns token list of the specified federation +// @Summary Get registration token list of the specified OpenFL federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=[]service.RegistrationTokenOpenFLListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/token [get] +func (controller *FederationController) listOpenFLToken(c *gin.Context) { + if tokens, err := func() ([]service.RegistrationTokenOpenFLListItem, error) { + federationUUID := c.Param("uuid") + return controller.federationApp.ListOpenFLToken(federationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: tokens, + } + c.JSON(http.StatusOK, resp) + } +} + +// createOpenFLToken creates a new registration token for an OpenFL federation +// @Summary Create a new registration token for an OpenFL federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param token body service.RegistrationTokenOpenFLBasicInfo true "The federation info" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/token [post] +func (controller *FederationController) createOpenFLToken(c *gin.Context) { + if err := func() error { + creationInfo := &service.RegistrationTokenOpenFLBasicInfo{} + if err := c.ShouldBindJSON(creationInfo); err != nil { + return err + } + federationUUID := c.Param("uuid") + return controller.federationApp.GeneratedOpenFLToken(creationInfo, federationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteOpenFLToken deletes the specified OpenFL federation registration token +// @Summary Delete an OpenFL federation registration token +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param tokenUUID path string true "token UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/token/{tokenUUID} [delete] +func (controller *FederationController) deleteOpenFLToken(c *gin.Context) { + uuid := c.Param("tokenUUID") + federationUUID := c.Param("uuid") + if err := controller.federationApp.DeleteOpenFLToken(uuid, federationUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFLDirectorDeploymentYAML returns deployment yaml content for deploying OpenFL director +// @Summary Get OpenFL director deployment yaml +// @Tags Federation +// @Produce json +// @Param chart_uuid query string true "the chart uuid" +// @Param federation_uuid query string true "the federation uuid" +// @Param name query string true "name of the deployment" +// @Param namespace query string true "namespace of the deployment" +// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort" +// @Param jupyter_password query string true "password to access the Jupyter Notebook" +// @Param registry query string true "customized registry address" +// @Param use_registry query bool true "choose if use the customized registry config" +// @Param use_registry_secret query bool true "choose if use the customized registry secret" +// @Param enable_psp query bool true "choose if enable the podSecurityPolicy" +// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/director/yaml [get] +func (controller *FederationController) getOpenFLDirectorDeploymentYAML(c *gin.Context) { + if yaml, err := func() (string, error) { + req := &domainService.ParticipantOpenFLDirectorYAMLCreationRequest{} + if err := c.ShouldBindQuery(req); err != nil { + return "", err + } + if err := c.ShouldBindQuery(&req.RegistryConfig); err != nil { + return "", err + } + return controller.participantAppService.GetOpenFLDirectorDeploymentYAML(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: yaml, + } + c.JSON(http.StatusOK, resp) + } +} + +// createOpenFLDirector creates a new OpenFL director +// @Summary Create a new OpenFL director +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param creationRequest body service.ParticipantOpenFLDirectorCreationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/director [post] +func (controller *FederationController) createOpenFLDirector(c *gin.Context) { + if uuid, err := func() (string, error) { + federationUUID := c.Param("uuid") + req := &domainService.ParticipantOpenFLDirectorCreationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + req.FederationUUID = federationUUID + return controller.participantAppService.CreateOpenFLDirector(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteOpenFLDirector deletes the specified OpenFL director +// @Summary Delete an OpenFL director +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param directorUUID path string true "director UUID" +// @Param force query bool false "if set to true, will try to remove the director forcefully" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/director/{directorUUID} [delete] +func (controller *FederationController) deleteOpenFLDirector(c *gin.Context) { + directorUUID := c.Param("directorUUID") + if err := func() error { + force, err := strconv.ParseBool(c.DefaultQuery("force", "false")) + if err != nil { + return err + } + return controller.participantAppService.RemoveOpenFLDirector(directorUUID, force) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFLParticipant returns participant list of the specified federation +// @Summary Get participant list of the specified OpenFL federation +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Success 200 {object} GeneralResponse{data=service.ParticipantOpenFLListInFederation} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/participant [get] +func (controller *FederationController) getOpenFLParticipant(c *gin.Context) { + if participants, err := func() (*service.ParticipantOpenFLListInFederation, error) { + federationUUID := c.Param("uuid") + return controller.participantAppService.GetOpenFLParticipantList(federationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: participants, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFLDirector returns detailed information of a OpenFL director +// @Summary Get specific info of OpenFL director +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param directorUUID path string true "director UUID" +// @Success 200 {object} GeneralResponse{data=service.OpenFLDirectorDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/director/{directorUUID} [get] +func (controller *FederationController) getOpenFLDirector(c *gin.Context) { + directorUUID := c.Param("directorUUID") + if directorDetail, err := controller.participantAppService.GetOpenFLDirectorDetail(directorUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: directorDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// registerOpenFLEnvoy handles envoy registration request +// @Summary Process Envoy registration request +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param registrationRequest body service.ParticipantOpenFLEnvoyRegistrationRequest true "The creation requests" +// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/envoy/register [post] +func (controller *FederationController) registerOpenFLEnvoy(c *gin.Context) { + if uuid, err := func() (string, error) { + req := &domainService.ParticipantOpenFLEnvoyRegistrationRequest{} + if err := c.ShouldBindJSON(req); err != nil { + return "", err + } + return controller.participantAppService.HandleOpenFLEnvoyRegistration(req) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} + +// deleteOpenFLEnvoy deletes the specified OpenFL envoy +// @Summary Delete an OpenFL envoy +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param envoyUUID path string true "envoy UUID" +// @Param force query bool false "if set to true, will try to envoy the director forcefully" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [delete] +func (controller *FederationController) deleteOpenFLEnvoy(c *gin.Context) { + envoyUUID := c.Param("envoyUUID") + if err := func() error { + force, err := strconv.ParseBool(c.DefaultQuery("force", "false")) + if err != nil { + return err + } + return controller.participantAppService.RemoveOpenFLEnvoy(envoyUUID, force) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFLEnvoy returns detailed information of a OpenFL envoy +// @Summary Get specific info of OpenFL envoy +// @Tags Federation +// @Produce json +// @Param uuid path string true "federation UUID" +// @Param envoyUUID path string true "envoy UUID" +// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [get] +func (controller *FederationController) getOpenFLEnvoy(c *gin.Context) { + envoyUUID := c.Param("envoyUUID") + if envoyDetail, err := controller.participantAppService.GetOpenFLEnvoyDetail(envoyUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: envoyDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// getOpenFLEnvoyWithToken returns detailed information of a OpenFL envoy, if the provided token is valid +// @Summary Get specific info of OpenFL envoy, by providing the envoy uuid and token string +// @Tags Federation +// @Produce json +// @Param uuid path string true "envoy UUID" +// @Param token query string true "token string" +// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /federation/openfl/envoy/{uuid} [get] +func (controller *FederationController) getOpenFLEnvoyWithToken(c *gin.Context) { + token := c.Query("token") + envoyUUID := c.Param("uuid") + if envoyDetail, err := controller.participantAppService.GetOpenFLEnvoyDetailWithTokenVerification(envoyUUID, token); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: envoyDetail, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/infra_provider.go b/server/api/infra_provider.go new file mode 100644 index 00000000..34d5ca61 --- /dev/null +++ b/server/api/infra_provider.go @@ -0,0 +1,226 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/gin-gonic/gin" +) + +// InfraProviderController provides API handlers for the infra provider related APIs +type InfraProviderController struct { + infraProviderAppService *service.InfraProviderApp +} + +// NewInfraProviderController returns a controller instance to handle infra provider API requests +func NewInfraProviderController(infraProviderKubernetesRepo repo.InfraProviderRepository, + endpointKubeFATERepo repo.EndpointRepository) *InfraProviderController { + return &InfraProviderController{ + infraProviderAppService: &service.InfraProviderApp{ + InfraProviderKubernetesRepo: infraProviderKubernetesRepo, + EndpointKubeFATERepo: endpointKubeFATERepo, + }, + } +} + +// Route sets up route mappings to infra provider related APIs +func (controller *InfraProviderController) Route(r *gin.RouterGroup) { + infraProvider := r.Group("infra") + infraProvider.Use(authMiddleware.MiddlewareFunc()) + { + infraProvider.GET("", controller.list) + infraProvider.POST("", controller.create) + infraProvider.POST("/kubernetes/connect", controller.testKubernetes) + + infraProvider.GET("/:uuid", controller.get) + infraProvider.DELETE("/:uuid", controller.delete) + infraProvider.PUT("/:uuid", controller.update) + } +} + +// list returns the provider list +// @Summary Return provider list data +// @Tags InfraProvider +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.InfraProviderListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra [get] +func (controller *InfraProviderController) list(c *gin.Context) { + providerList, err := controller.infraProviderAppService.GetProviderList() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: providerList, + } + c.JSON(http.StatusOK, resp) + } +} + +// create a new provider +// @Summary Create a new infra provider +// @Tags InfraProvider +// @Produce json +// @Param provider body service.InfraProviderCreationRequest true "The provider information, currently for the type field only 'Kubernetes' is supported" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra [post] +func (controller *InfraProviderController) create(c *gin.Context) { + if err := func() error { + providerInfo := &service.InfraProviderCreationRequest{} + if err := c.ShouldBindJSON(providerInfo); err != nil { + return err + } + return controller.infraProviderAppService.CreateProvider(providerInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// testKubernetes test connection to Kubernetes infra provider +// @Summary Test connection to a Kubernetes infra provider +// @Tags InfraProvider +// @Produce json +// @Param permission body valueobject.KubeConfig true "The kubeconfig content" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra/kubernetes/connect [post] +func (controller *InfraProviderController) testKubernetes(c *gin.Context) { + if err := func() error { + kubeconfig := &valueobject.KubeConfig{} + if err := c.ShouldBindJSON(kubeconfig); err != nil { + return err + } + return controller.infraProviderAppService.TestKubernetesConnection(kubeconfig) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of an infra provider +// @Summary Get infra provider's detailed info +// @Tags InfraProvider +// @Produce json +// @Param uuid path string true "Provider UUID" +// @Success 200 {object} GeneralResponse{data=service.InfraProviderDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra/{uuid} [get] +func (controller *InfraProviderController) get(c *gin.Context) { + uuid := c.Param("uuid") + if providerDetail, err := controller.infraProviderAppService.GetProviderDetail(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: providerDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// delete the infra provider +// @Summary Delete the infra provider +// @Tags InfraProvider +// @Produce json +// @Param uuid path string true "Provider UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra/{uuid} [delete] +func (controller *InfraProviderController) delete(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.infraProviderAppService.DeleteProvider(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// update the provider configuration +// @Summary Updates the infra provider +// @Tags InfraProvider +// @Produce json +// @Param uuid path string true "Provider UUID" +// @Param provider body service.InfraProviderUpdateRequest true "The updated provider information" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /infra/{uuid} [put] +func (controller *InfraProviderController) update(c *gin.Context) { + if err := func() error { + uuid := c.Param("uuid") + providerInfo := &service.InfraProviderUpdateRequest{} + if err := c.ShouldBindJSON(providerInfo); err != nil { + return err + } + return controller.infraProviderAppService.UpdateProvider(uuid, providerInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/api/types.go b/server/api/types.go new file mode 100644 index 00000000..c3427191 --- /dev/null +++ b/server/api/types.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// GeneralResponse is used for the API response +type GeneralResponse struct { + Code int `json:"code" example:"0"` + Message string `json:"message" example:"success"` + Data interface{} `json:"data" swaggertype:"object"` +} diff --git a/server/api/user.go b/server/api/user.go new file mode 100644 index 00000000..b0d80bf9 --- /dev/null +++ b/server/api/user.go @@ -0,0 +1,140 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/server/application/service" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/repo" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" +) + +// UserController manages user related API calls +type UserController struct { + userAppService *service.UserApp +} + +// NewUserController returns a controller instance to handle user API requests +func NewUserController(repo repo.UserRepository) *UserController { + return &UserController{ + userAppService: &service.UserApp{ + UserRepo: repo, + }, + } +} + +// Route set up route mappings to user related APIs +func (controller *UserController) Route(r *gin.RouterGroup) { + users := r.Group("user") + { + users.POST("/login", controller.login) + users.POST("/logout", controller.logout) + } + users.Use(authMiddleware.MiddlewareFunc()) + { + users.GET("/current", controller.getCurrentUsername) + users.PUT("/:id/password", controller.updatePassword) + } +} + +// login to lifecycle manager using the provided credentials +// @Summary login to lifecycle manager +// @Tags User +// @Produce json +// @Param credentials body service.LoginInfo true "credentials for login" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Router /user/login [post] +func (controller *UserController) login(c *gin.Context) { + authMiddleware.LoginHandler(c) +} + +// logout from the lifecycle manager +// @Summary logout from the lifecycle manager +// @Tags User +// @Produce json +// @Success 200 {object} GeneralResponse "Success" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/logout [post] +func (controller *UserController) logout(c *gin.Context) { + authMiddleware.LogoutHandler(c) +} + +// getCurrentUser return current user +// @Summary Return current user in the jwt token +// @Tags User +// @Produce json +// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/current [get] +func (controller *UserController) getCurrentUsername(c *gin.Context) { + if username, err := func() (string, error) { + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + username := claims[nameKey].(string) + return username, nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: username, + } + c.JSON(http.StatusOK, resp) + } +} + +// updatePassword update user password +// @Summary Update user password +// @Tags User +// @Produce json +// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/{userId}/password [put] +func (controller *UserController) updatePassword(c *gin.Context) { + if err := func() error { + userId, err := strconv.Atoi(c.Param("id")) + if err != nil { + return err + } + passwordChangeInfo := &service.PwdChangeInfo{} + if err := c.ShouldBindJSON(&passwordChangeInfo); err != nil { + return err + } + return controller.userAppService.UpdateUserPassword(userId, passwordChangeInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/server/application/service/certificate_authority_service.go b/server/application/service/certificate_authority_service.go new file mode 100644 index 00000000..bbb1c60b --- /dev/null +++ b/server/application/service/certificate_authority_service.go @@ -0,0 +1,216 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + "io/ioutil" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + "github.com/spf13/viper" +) + +// CertificateAuthorityApp provides functions to manage the Certificate Authority +type CertificateAuthorityApp struct { + CertificateAuthorityRepo repo.CertificateAuthorityRepository +} + +// CertificateAuthorityDetail contains the detail info of Certificate Authority +type CertificateAuthorityDetail struct { + CertificateAuthorityEditableItem + UUID string `json:"uuid"` + CreatedAt time.Time `json:"created_at"` + Status CertificateAuthorityStatus `json:"status"` + StatusMessage string `json:"status_message"` +} + +// CertificateAuthorityEditableItem contains properties of a CA that should be provided by the user +type CertificateAuthorityEditableItem struct { + Name string `json:"name"` + Description string `json:"description"` + Type entity.CertificateAuthorityType `json:"type"` + Config map[string]interface{} `json:"config"` +} + +// CertificateAuthorityStatus is the certificate authority status +type CertificateAuthorityStatus uint8 + +const ( + CertificateAuthorityStatusUnknown CertificateAuthorityStatus = iota + CertificateAuthorityStatusUnhealthy + CertificateAuthorityStatusHealthy +) + +// Get returns the current CA configuration +func (app *CertificateAuthorityApp) Get() (*CertificateAuthorityDetail, error) { + instance, err := app.CertificateAuthorityRepo.GetFirst() + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, nil + } + return nil, err + } + ca := instance.(*entity.CertificateAuthority) + // check the ca status + caStatus := CertificateAuthorityStatusUnknown + caStatusMessage := "" + switch ca.Type { + case entity.CertificateAuthorityTypeStepCA: + _, err = ca.Client() + if err != nil { + caStatus = CertificateAuthorityStatusUnhealthy + caStatusMessage = err.Error() + } else { + caStatus = CertificateAuthorityStatusHealthy + } + } + + var config map[string]interface{} + err = json.Unmarshal([]byte(ca.ConfigurationJSON), &config) + if err != nil { + return nil, err + } + return &CertificateAuthorityDetail{ + CertificateAuthorityEditableItem: CertificateAuthorityEditableItem{ + Name: ca.Name, + Description: ca.Description, + Type: ca.Type, + Config: config, + }, + UUID: ca.UUID, + CreatedAt: ca.CreatedAt, + Status: caStatus, + StatusMessage: caStatusMessage, + }, nil + +} + +// CreateCA creates a certificate authority +func (app *CertificateAuthorityApp) CreateCA(caInfo *CertificateAuthorityEditableItem) error { + //check CA exist + instance, _ := app.CertificateAuthorityRepo.GetFirst() + if instance != nil { + return errors.Errorf("certificate authority already exists") + } + switch caInfo.Type { + case entity.CertificateAuthorityTypeStepCA: + //Decode map, remove unmapped keys + var config entity.CertificateAuthorityConfigurationStepCA + err := mapstructure.Decode(caInfo.Config, &config) + if err != nil { + return err + } + // validate URL schema + u, err := url.ParseRequestURI(config.ServiceURL) + if err != nil || u.Scheme == "" && u.Host == "" { + return errors.Errorf("Service URL is invalid: http:// or https:// schema is required") + } + // marshal to json + caConfig, err := json.Marshal(config) + if err != nil { + return err + } + ca := &entity.CertificateAuthority{ + UUID: uuid.NewV4().String(), + Name: caInfo.Name, + Description: caInfo.Description, + Type: caInfo.Type, + ConfigurationJSON: string(caConfig), + } + // validate CA config info + _, err = ca.Client() + if err != nil { + return err + } + return app.CertificateAuthorityRepo.Create(ca) + } + return errors.Errorf("unknown certificate authority type: %v", caInfo.Type) +} + +// Update changes CA settings +func (app *CertificateAuthorityApp) Update(uuid string, caInfo *CertificateAuthorityEditableItem) error { + switch caInfo.Type { + case entity.CertificateAuthorityTypeStepCA: + var config entity.CertificateAuthorityConfigurationStepCA + // decode map, remove unmapped keys + err := mapstructure.Decode(caInfo.Config, &config) + if err != nil { + return err + } + // validate URL schema + u, err := url.ParseRequestURI(config.ServiceURL) + if err != nil || u.Scheme == "" && u.Host == "" { + return errors.Errorf("Service URL is invalid: http:// or https:// schema is required") + } + // marshal to json + caConfig, err := json.Marshal(config) + if err != nil { + return err + } + ca := &entity.CertificateAuthority{ + UUID: uuid, + Name: caInfo.Name, + Description: caInfo.Description, + Type: caInfo.Type, + ConfigurationJSON: string(caConfig), + } + // validate CA config info + _, err = ca.Client() + if err != nil { + return err + } + return app.CertificateAuthorityRepo.UpdateByUUID(ca) + } + return errors.Errorf("unknown certificate authority type: %v", caInfo.Type) +} + +// GetBuiltInCAConfig returns the config of built-in StepCA config +// Unnecessary to move to domain package due to simple logic +func (app *CertificateAuthorityApp) GetBuiltInCAConfig() (*entity.CertificateAuthorityConfigurationStepCA, error) { + serverURL := viper.GetString("lifecyclemanager.builtinca.host") + provisionerName := viper.GetString("lifecyclemanager.builtinca.provisioner.name") + password := viper.GetString("lifecyclemanager.builtinca.provisioner.password") + pemDir := viper.GetString("lifecyclemanager.builtinca.datadir") + + if serverURL == "" { + return nil, errors.Errorf("fail to get the built-in CA config: missing server name") + } + if password == "" { + return nil, errors.Errorf("fail to get the built-in CA config: missing provisioner password") + } + if provisionerName == "" { + provisionerName = "admin" + } + pemPath := filepath.Join(pemDir, "certs/root_ca.crt") + pem, err := ioutil.ReadFile(pemPath) + if err != nil { + return nil, errors.Wrap(err, "fail to get the root certificate of built-in CA.") + } + config := &entity.CertificateAuthorityConfigurationStepCA{ + ServiceURL: serverURL, + ServiceCertificatePEM: string(pem), + ProvisionerName: provisionerName, + ProvisionerPassword: password, + } + return config, nil +} diff --git a/server/application/service/certificate_service.go b/server/application/service/certificate_service.go new file mode 100644 index 00000000..8575a0b7 --- /dev/null +++ b/server/application/service/certificate_service.go @@ -0,0 +1,143 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "fmt" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// CertificateApp provides functions to manage the certificates +type CertificateApp struct { + CertificateAuthorityRepo repo.CertificateAuthorityRepository + CertificateRepo repo.CertificateRepository + CertificateBindingRepo repo.CertificateBindingRepository + ParticipantFATERepo repo.ParticipantFATERepository + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + FederationFATERepo repo.FederationRepository + FederationOpenFLRepo repo.FederationRepository +} + +// CertificateListItem contains basic info of a certificate +type CertificateListItem struct { + Name string `json:"name"` + UUID string `json:"uuid"` + SerialNumber string `json:"serial_number"` + ExpirationDate time.Time `json:"expiration_date"` + CommonName string `json:"common_name"` + Bindings []CertificateBindingListItem `json:"bindings"` +} + +// CertificateBindingListItem contains binding information of a certificate +type CertificateBindingListItem struct { + ServiceType entity.CertificateBindingServiceType `json:"service_type"` + ParticipantUUID string `json:"participant_uuid"` + ParticipantName string `json:"participant_name"` + ParticipantType string `json:"participant_type"` + ServiceDescription string `json:"service_description"` + FederationUUID string `json:"federation_uuid"` + FederationName string `json:"federation_name"` + FederationType entity.FederationType `json:"federation_type"` +} + +// List returns the currently managed certificates +func (app *CertificateApp) List() (certificateList []CertificateListItem, err error) { + instanceList, err := app.CertificateRepo.List() + if err != nil { + return + } + domainCertList := instanceList.([]entity.Certificate) + for _, domainCert := range domainCertList { + cert := CertificateListItem{ + Name: domainCert.Name, + UUID: domainCert.UUID, + SerialNumber: domainCert.SerialNumber.String(), + ExpirationDate: domainCert.NotAfter, + CommonName: domainCert.Subject.CommonName, + Bindings: []CertificateBindingListItem{}, + } + instanceList, err = app.CertificateBindingRepo.ListByCertificateUUID(domainCert.UUID) + if err != nil { + return + } + bindingList := instanceList.([]entity.CertificateBinding) + for _, domainBinding := range bindingList { + binding := CertificateBindingListItem{ + ServiceType: domainBinding.ServiceType, + ParticipantUUID: domainBinding.ParticipantUUID, + ParticipantName: "Unknown", + ServiceDescription: "Unknown", + FederationUUID: domainBinding.FederationUUID, + FederationType: domainBinding.FederationType, + } + switch domainBinding.FederationType { + case entity.FederationTypeFATE: + instance, err := app.ParticipantFATERepo.GetByUUID(binding.ParticipantUUID) + if err == nil { + participant := instance.(*entity.ParticipantFATE) + binding.ParticipantName = participant.Name + binding.ParticipantType = participant.Type.String() + binding.ServiceDescription = getBindingFATEServiceDescription(domainBinding.ServiceType, participant) + } + federationInstance, err := app.FederationFATERepo.GetByUUID(domainBinding.FederationUUID) + if err == nil { + federation := federationInstance.(*entity.FederationFATE) + binding.FederationName = federation.Name + } + case entity.FederationTypeOpenFL: + instance, err := app.ParticipantOpenFLRepo.GetByUUID(binding.ParticipantUUID) + if err == nil { + participant := instance.(*entity.ParticipantOpenFL) + binding.ParticipantName = participant.Name + binding.ParticipantType = participant.Type.String() + binding.ServiceDescription = getBindingOpenFLServiceDescription(domainBinding.ServiceType, participant) + } + federationInstance, err := app.FederationOpenFLRepo.GetByUUID(domainBinding.FederationUUID) + if err == nil { + federation := federationInstance.(*entity.FederationOpenFL) + binding.FederationName = federation.Name + } + } + cert.Bindings = append(cert.Bindings, binding) + } + certificateList = append(certificateList, cert) + } + return +} + +func getBindingFATEServiceDescription(serviceType entity.CertificateBindingServiceType, participant *entity.ParticipantFATE) string { + return fmt.Sprintf("%v service of FATE %v", serviceType, participant.Type) +} + +func getBindingOpenFLServiceDescription(serviceType entity.CertificateBindingServiceType, participant *entity.ParticipantOpenFL) string { + return fmt.Sprintf("%v service of OpenFL %v", serviceType, participant.Type) +} + +// DeleteCertificate deletes the certificate which has no participants bindings +func (app *CertificateApp) DeleteCertificate(uuid string) error { + instanceList, err := app.CertificateBindingRepo.ListByCertificateUUID(uuid) + if err != nil { + return errors.Errorf("unable to get the certificate bindings") + } + bindingList := instanceList.([]entity.CertificateBinding) + if len(bindingList) > 0 { + return errors.Errorf("unable to delete certificate: there is(are) %v particicant(s) still binding to this certificate", len(bindingList)) + } + return app.CertificateRepo.DeleteByUUID(uuid) +} diff --git a/server/application/service/chart_service.go b/server/application/service/chart_service.go new file mode 100644 index 00000000..bd1ee8a6 --- /dev/null +++ b/server/application/service/chart_service.go @@ -0,0 +1,103 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// ChartApp provide functions to manage the available helm charts +type ChartApp struct { + ChartRepo repo.ChartRepository +} + +// ChartListItem contains basic info of a chart that can be used in a "list" view +type ChartListItem struct { + UUID string `json:"uuid"` + Name string `json:"name"` + ChartName string `json:"chart_name"` + Version string `json:"version"` + Description string `json:"description"` + Type entity.ChartType `json:"type"` + CreatedAt time.Time `json:"created_at"` + ContainPortalServices bool `json:"contain_portal_services"` +} + +// ChartDetail contains detailed info of a chart +type ChartDetail struct { + ChartListItem + About string `json:"about"` + Values string `json:"values"` + ValuesTemplate string `json:"values_template"` +} + +// List returns the currently installed chart +func (app *ChartApp) List(t entity.ChartType) ([]ChartListItem, error) { + var chartList []ChartListItem + var domainChartList []entity.Chart + if t == entity.ChartTypeUnknown { + instanceList, err := app.ChartRepo.List() + if err != nil { + return nil, err + } + domainChartList = instanceList.([]entity.Chart) + } else { + instanceList, err := app.ChartRepo.ListByType(t) + if err != nil { + return nil, err + } + domainChartList = instanceList.([]entity.Chart) + } + for _, domainChart := range domainChartList { + chartList = append(chartList, ChartListItem{ + UUID: domainChart.UUID, + Name: domainChart.Name, + ChartName: domainChart.ChartName, + Version: domainChart.Version, + Description: domainChart.Description, + Type: domainChart.Type, + CreatedAt: domainChart.CreatedAt, + ContainPortalServices: domainChart.Private, + }) + } + return chartList, nil +} + +// GetDetail returns detailed info of a chart +func (app *ChartApp) GetDetail(uuid string) (*ChartDetail, error) { + instance, err := app.ChartRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + domainChart := instance.(*entity.Chart) + return &ChartDetail{ + ChartListItem: ChartListItem{ + UUID: domainChart.UUID, + Name: domainChart.Name, + ChartName: domainChart.ChartName, + Version: domainChart.Version, + Description: domainChart.Description, + Type: domainChart.Type, + CreatedAt: domainChart.CreatedAt, + ContainPortalServices: domainChart.Private, + }, + About: domainChart.Chart, + Values: domainChart.Values, + ValuesTemplate: domainChart.ValuesTemplate, + }, nil +} diff --git a/server/application/service/endpoint_service.go b/server/application/service/endpoint_service.go new file mode 100644 index 00000000..599aa1a3 --- /dev/null +++ b/server/application/service/endpoint_service.go @@ -0,0 +1,232 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "fmt" + "time" + + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/service" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// EndpointApp provide functions to manage the endpoints +type EndpointApp struct { + InfraProviderKubernetesRepo repo.InfraProviderRepository + EndpointKubeFAETRepo repo.EndpointRepository + ParticipantFATERepo repo.ParticipantFATERepository + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + EventRepo repo.EventRepository +} + +// EndpointListItem contains basic information of an endpoint +type EndpointListItem struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Description string `json:"description"` + Type entity.EndpointType `json:"type"` + CreatedAt time.Time `json:"created_at"` + InfraProviderName string `json:"infra_provider_name"` + InfraProviderUUID string `json:"infra_provider_uuid"` + KubeFATEHost string `json:"kubefate_host"` + KubeFATEAddress string `json:"kubefate_address"` + KubeFATEVersion string `json:"kubefate_version"` + Status entity.EndpointStatus `json:"status"` +} + +// EndpointDetail contains basic information of an endpoint as well as additional information +type EndpointDetail struct { + EndpointListItem + KubeFATEDeploymentYAML string `json:"kubefate_deployment_yaml"` +} + +// EndpointScanItem contains basic information of an endpoint that is scanned from an infra provider +type EndpointScanItem struct { + EndpointListItem + IsManaged bool `json:"is_managed"` + IsCompatible bool `json:"is_compatible"` +} + +// EndpointScanRequest contains necessary request info to start an endpoint scan +type EndpointScanRequest struct { + InfraProviderUUID string `json:"infra_provider_uuid"` + Type entity.EndpointType `json:"type"` +} + +// EndpointCreationRequest contains necessary request info to create an endpoint +type EndpointCreationRequest struct { + EndpointScanRequest + Name string `json:"name"` + Description string `json:"description"` + Install bool `json:"install"` + IngressControllerServiceMode entity.EndpointKubeFATEIngressControllerServiceMode `json:"ingress_controller_service_mode"` + KubeFATEDeploymentYAML string `json:"kubefate_deployment_yaml"` +} + +// GetEndpointList returns currently managed endpoints +func (app *EndpointApp) GetEndpointList() ([]EndpointListItem, error) { + endpointListInstance, err := app.EndpointKubeFAETRepo.List() + if err != nil { + return nil, errors.Wrapf(err, "failed to get KubeFATE endpoint list") + } + domainEndpointList := endpointListInstance.([]entity.EndpointKubeFATE) + + var endpointList []EndpointListItem + for _, domainEndpointKubeFATE := range domainEndpointList { + endpoint := EndpointListItem{ + UUID: domainEndpointKubeFATE.UUID, + Name: domainEndpointKubeFATE.Name, + Description: domainEndpointKubeFATE.Description, + Type: entity.EndpointTypeKubeFATE, + CreatedAt: domainEndpointKubeFATE.CreatedAt, + InfraProviderName: "Unknown", + InfraProviderUUID: fmt.Sprintf("Unknown (%s)", domainEndpointKubeFATE.InfraProviderUUID), + KubeFATEHost: domainEndpointKubeFATE.Config.IngressRuleHost, + KubeFATEAddress: domainEndpointKubeFATE.Config.IngressAddress, + KubeFATEVersion: domainEndpointKubeFATE.Version, + Status: domainEndpointKubeFATE.Status, + } + domainProviderInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(domainEndpointKubeFATE.InfraProviderUUID) + if err != nil { + log.Err(err).Msg("failed to query provider") + } else { + domainProvider := domainProviderInstance.(*entity.InfraProviderKubernetes) + endpoint.InfraProviderUUID = domainProvider.UUID + endpoint.InfraProviderName = domainProvider.Name + } + endpointList = append(endpointList, endpoint) + } + return endpointList, nil +} + +// GetEndpointDetail returns detailed information of an endpoint +func (app *EndpointApp) GetEndpointDetail(uuid string) (*EndpointDetail, error) { + endpointInstance, err := app.EndpointKubeFAETRepo.GetByUUID(uuid) + if err != nil { + return nil, errors.Wrapf(err, "failed to get KubeFAET endpoint instance") + } + domainEndpointKubeFATE := endpointInstance.(*entity.EndpointKubeFATE) + + endpoint := &EndpointDetail{ + EndpointListItem: EndpointListItem{ + UUID: domainEndpointKubeFATE.UUID, + Name: domainEndpointKubeFATE.Name, + Description: domainEndpointKubeFATE.Description, + Type: entity.EndpointTypeKubeFATE, + CreatedAt: domainEndpointKubeFATE.CreatedAt, + InfraProviderName: "Unknown", + InfraProviderUUID: fmt.Sprintf("Unknown (%s)", domainEndpointKubeFATE.InfraProviderUUID), + KubeFATEHost: domainEndpointKubeFATE.Config.IngressRuleHost, + KubeFATEAddress: domainEndpointKubeFATE.Config.IngressAddress, + KubeFATEVersion: domainEndpointKubeFATE.Version, + Status: domainEndpointKubeFATE.Status, + }, + KubeFATEDeploymentYAML: domainEndpointKubeFATE.DeploymentYAML, + } + domainProviderInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(domainEndpointKubeFATE.InfraProviderUUID) + if err != nil { + log.Err(err).Msg("failed to query provider") + } else { + domainProvider := domainProviderInstance.(*entity.InfraProviderKubernetes) + endpoint.InfraProviderUUID = domainProvider.UUID + endpoint.InfraProviderName = domainProvider.Name + } + return endpoint, nil +} + +// DeleteEndpoint removes the endpoint +func (app *EndpointApp) DeleteEndpoint(uuid string, uninstall bool) error { + return app.getDomainService().RemoveEndpoint(uuid, uninstall) +} + +// ScanEndpoint returns endpoints installed in an infra provider +func (app *EndpointApp) ScanEndpoint(req *EndpointScanRequest) ([]EndpointScanItem, error) { + if req.InfraProviderUUID == "" { + return nil, errors.New("missing providerUUID") + } + if req.Type == "" { + return nil, errors.New("missing endpointType") + } + + domainProviderInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(req.InfraProviderUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to query provider") + } + domainProvider := domainProviderInstance.(*entity.InfraProviderKubernetes) + + scanResult, err := app.getDomainService().FindKubeFATEEndpoint(req.InfraProviderUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to scan the endpoints") + } + var itemList []EndpointScanItem + + for _, resultItem := range scanResult { + itemList = append(itemList, EndpointScanItem{ + EndpointListItem: EndpointListItem{ + UUID: resultItem.UUID, + Name: resultItem.Name, + Description: resultItem.Description, + Type: resultItem.Type, + CreatedAt: resultItem.CreatedAt, + InfraProviderName: domainProvider.Name, + InfraProviderUUID: domainProvider.UUID, + // ignore kubefate related info + KubeFATEHost: "", + KubeFATEAddress: "", + Status: resultItem.Status, + }, + IsManaged: resultItem.IsManaged, + IsCompatible: resultItem.IsCompatible, + }) + } + return itemList, nil +} + +// CheckKubeFATEConnection tests connection to a KubeFATE endpoint +func (app *EndpointApp) CheckKubeFATEConnection(uuid string) error { + return app.getDomainService().TestKubeFATE(uuid) +} + +// GetKubeFATEDeploymentYAML returns the default yaml content for deploying KubeFATE +func (app *EndpointApp) GetKubeFATEDeploymentYAML(serviceUsername, servicePassword, hostname string, registryConfig valueobject.KubeRegistryConfig) (string, error) { + return app.getDomainService().GetDeploymentYAML(serviceUsername, servicePassword, hostname, registryConfig) +} + +// CreateEndpoint add or install an endpoint +func (app *EndpointApp) CreateEndpoint(req *EndpointCreationRequest) (string, error) { + switch req.Type { + case entity.EndpointTypeKubeFATE: + return app.getDomainService().CreateKubeFATEEndpoint(req.InfraProviderUUID, req.Name, req.Description, req.KubeFATEDeploymentYAML, req.Install, req.IngressControllerServiceMode) + default: + return "", constants.ErrNotImplemented + } +} + +func (app *EndpointApp) getDomainService() *service.EndpointService { + return &service.EndpointService{ + InfraProviderKubernetesRepo: app.InfraProviderKubernetesRepo, + EndpointKubeFATERepo: app.EndpointKubeFAETRepo, + ParticipantFATERepo: app.ParticipantFATERepo, + ParticipantOpenFLRepo: app.ParticipantOpenFLRepo, + EventService: &service.EventService{ + EventRepo: app.EventRepo, + }, + } +} diff --git a/server/application/service/event_service.go b/server/application/service/event_service.go new file mode 100644 index 00000000..9e237c90 --- /dev/null +++ b/server/application/service/event_service.go @@ -0,0 +1,65 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// EventApp provide functions to manage the events +type EventApp struct { + EventRepo repo.EventRepository +} + +// EventListItem contains basic information of an event +type EventListItem struct { + UUID string `json:"uuid"` + Type entity.EventType `json:"type"` + CreatedAt time.Time `json:"created_at"` + EntityUUID string `json:"entity_uuid"` + EntityType entity.EntityType `json:"entity_type"` + Data entity.EventData `json:"data"` +} + +// GetEventList returns events of a entity +func (app *EventApp) GetEventList(entity_uuid string) ([]EventListItem, error) { + eventInstanceList, err := app.EventRepo.ListByEntityUUID(entity_uuid) + if err != nil { + return nil, err + } + domainEventList := eventInstanceList.([]entity.Event) + + var eventList []EventListItem + for _, domainEvent := range domainEventList { + var data entity.EventData + err = json.Unmarshal([]byte(domainEvent.Data), &data) + if err != nil { + return nil, err + } + eventList = append(eventList, EventListItem{ + UUID: domainEvent.UUID, + Type: domainEvent.Type, + CreatedAt: domainEvent.CreatedAt, + EntityUUID: domainEvent.EntityUUID, + EntityType: domainEvent.EntityType, + Data: data, + }) + } + return eventList, nil +} diff --git a/server/application/service/federation_openfl_service.go b/server/application/service/federation_openfl_service.go new file mode 100644 index 00000000..2e7b19cb --- /dev/null +++ b/server/application/service/federation_openfl_service.go @@ -0,0 +1,165 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" +) + +// FederationOpenFLCreationRequest contains necessary info to create an OpenFL federation +type FederationOpenFLCreationRequest struct { + FederationCreationRequest + Domain string `json:"domain"` + UseCustomizedShardDescriptor bool `json:"use_customized_shard_descriptor"` + ShardDescriptorConfig *valueobject.ShardDescriptorConfig `json:"shard_descriptor_config"` +} + +// FederationOpenFLDetail contains specific info for an OpenFL federation +type FederationOpenFLDetail struct { + FederationListItem + Domain string `json:"domain"` + UseCustomizedShardDescriptor bool `json:"use_customized_shard_descriptor"` + ShardDescriptorConfig *valueobject.ShardDescriptorConfig `json:"shard_descriptor_config"` +} + +// RegistrationTokenOpenFLBasicInfo contains necessary info to generate token for an OpenFL federation +type RegistrationTokenOpenFLBasicInfo struct { + Name string `json:"name"` + Description string `json:"description"` + ExpiredAt time.Time `json:"expired_at"` + Limit int `json:"limit"` + Used int `json:"used"` + Labels valueobject.Labels `json:"labels"` +} + +type RegistrationTokenOpenFLListItem struct { + RegistrationTokenOpenFLBasicInfo + UUID string `json:"uuid"` + DisplayedTokenStr string `json:"token_str"` +} + +// CreateOpenFLFederation creates an OpenFL federation entity +func (app *FederationApp) CreateOpenFLFederation(req *FederationOpenFLCreationRequest) (string, error) { + federation := &entity.FederationOpenFL{ + Federation: entity.Federation{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + Type: entity.FederationTypeOpenFL, + Repo: app.FederationOpenFLRepo, + }, + Domain: req.Domain, + UseCustomizedShardDescriptor: req.UseCustomizedShardDescriptor, + ShardDescriptorConfig: req.ShardDescriptorConfig, + } + if err := federation.Create(); err != nil { + return "", err + } + return federation.UUID, nil +} + +// DeleteOpenFLFederation deletes an OpenFL federation +func (app *FederationApp) DeleteOpenFLFederation(uuid string) error { + //check participant existence + instanceList, err := app.ParticipantOpenFLRepo.ListByFederationUUID(uuid) + if err != nil { + return errors.Wrap(err, "failed to list federation participants") + } + participantList := instanceList.([]entity.ParticipantOpenFL) + if len(participantList) > 0 { + return errors.Errorf("cannot remove federation that still contains %v OpenFL participants", len(participantList)) + } + if err := app.RegistrationTokenOpenFLRepo.DeleteByFederation(uuid); err != nil { + return errors.Wrap(err, "failed to clean up tokens") + } + return app.FederationOpenFLRepo.DeleteByUUID(uuid) +} + +// GetOpenFLFederation returns basic info of a specific OpenFL federation +func (app *FederationApp) GetOpenFLFederation(uuid string) (*FederationOpenFLDetail, error) { + instance, err := app.FederationOpenFLRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + domainFederation := instance.(*entity.FederationOpenFL) + return &FederationOpenFLDetail{ + FederationListItem: FederationListItem{ + UUID: domainFederation.UUID, + Name: domainFederation.Name, + Description: domainFederation.Description, + Type: domainFederation.Type, + CreatedAt: domainFederation.CreatedAt, + }, + Domain: domainFederation.Domain, + UseCustomizedShardDescriptor: domainFederation.UseCustomizedShardDescriptor, + ShardDescriptorConfig: domainFederation.ShardDescriptorConfig, + }, nil +} + +// GeneratedOpenFLToken creates a new token for an OpenFL federation +func (app *FederationApp) GeneratedOpenFLToken(req *RegistrationTokenOpenFLBasicInfo, federationUUID string) error { + token := &entity.RegistrationTokenOpenFL{ + RegistrationToken: entity.RegistrationToken{ + Name: req.Name, + Description: req.Description, + Repo: app.RegistrationTokenOpenFLRepo, + }, + ExpiredAt: req.ExpiredAt, + Limit: req.Limit, + Labels: req.Labels, + FederationUUID: federationUUID, + } + return token.Create() +} + +// ListOpenFLToken list all registration tokens in an OpenFL federation +func (app *FederationApp) ListOpenFLToken(federationUUID string) ([]RegistrationTokenOpenFLListItem, error) { + instanceList, err := app.RegistrationTokenOpenFLRepo.ListByFederation(federationUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query token list") + } + domainTokenList := instanceList.([]entity.RegistrationTokenOpenFL) + var tokenList []RegistrationTokenOpenFLListItem + for _, token := range domainTokenList { + count, err := app.ParticipantOpenFLRepo.CountByTokenUUID(token.UUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query token count") + } + tokenList = append(tokenList, RegistrationTokenOpenFLListItem{ + RegistrationTokenOpenFLBasicInfo: RegistrationTokenOpenFLBasicInfo{ + Name: token.Name, + Description: token.Description, + ExpiredAt: token.ExpiredAt, + Limit: token.Limit, + Used: count, + Labels: token.Labels, + }, + UUID: token.UUID, + DisplayedTokenStr: token.Display(), + }) + } + return tokenList, nil +} + +// DeleteOpenFLToken removes the specified token +func (app *FederationApp) DeleteOpenFLToken(uuid, federationUUID string) error { + // TODO: validate federation UUID + return app.RegistrationTokenOpenFLRepo.DeleteByUUID(uuid) +} diff --git a/server/application/service/federation_service.go b/server/application/service/federation_service.go new file mode 100644 index 00000000..a6ed4ea7 --- /dev/null +++ b/server/application/service/federation_service.go @@ -0,0 +1,146 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" +) + +// FederationApp provides application level API for federation related actions +type FederationApp struct { + FederationFATERepo repo.FederationRepository + ParticipantFATERepo repo.ParticipantFATERepository + FederationOpenFLRepo repo.FederationRepository + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + RegistrationTokenOpenFLRepo repo.RegistrationTokenRepository +} + +// FederationListItem contains basic info of a federation +type FederationListItem struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Description string `json:"description"` + Type entity.FederationType `json:"type"` + CreatedAt time.Time `json:"created_at"` +} + +// FederationCreationRequest contains basic info for creating a federation +type FederationCreationRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// FederationFATECreationRequest contains necessary info to create a FATE federation +type FederationFATECreationRequest struct { + FederationCreationRequest + Domain string `json:"domain"` +} + +// FederationFATEDetail contains specific info for a FATE federation +type FederationFATEDetail struct { + FederationListItem + Domain string `json:"domain"` +} + +// List returns all saved federation +func (app *FederationApp) List() ([]FederationListItem, error) { + instanceList, err := app.FederationFATERepo.List() + if err != nil { + return nil, err + } + domainFederationList := instanceList.([]entity.FederationFATE) + var federationList []FederationListItem + for _, domainFederation := range domainFederationList { + federationList = append(federationList, FederationListItem{ + UUID: domainFederation.UUID, + Name: domainFederation.Name, + Description: domainFederation.Description, + Type: domainFederation.Type, + CreatedAt: domainFederation.CreatedAt, + }) + } + + instanceList, err = app.FederationOpenFLRepo.List() + if err != nil { + return nil, err + } + domainFederationOpenFLList := instanceList.([]entity.FederationOpenFL) + for _, domainFederation := range domainFederationOpenFLList { + federationList = append(federationList, FederationListItem{ + UUID: domainFederation.UUID, + Name: domainFederation.Name, + Description: domainFederation.Description, + Type: domainFederation.Type, + CreatedAt: domainFederation.CreatedAt, + }) + } + return federationList, nil +} + +// GetFATEFederation returns basic info of a specific FATE federation +func (app *FederationApp) GetFATEFederation(uuid string) (*FederationFATEDetail, error) { + instance, err := app.FederationFATERepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + domainFederation := instance.(*entity.FederationFATE) + return &FederationFATEDetail{ + FederationListItem: FederationListItem{ + UUID: domainFederation.UUID, + Name: domainFederation.Name, + Description: domainFederation.Description, + Type: domainFederation.Type, + CreatedAt: domainFederation.CreatedAt, + }, + Domain: domainFederation.Domain, + }, nil +} + +// CreateFATEFederation creates a FATE federation +func (app *FederationApp) CreateFATEFederation(req *FederationFATECreationRequest) (string, error) { + federation := &entity.FederationFATE{ + Federation: entity.Federation{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + Type: entity.FederationTypeFATE, + Repo: app.FederationFATERepo, + }, + Domain: req.Domain, + } + if err := federation.Create(); err != nil { + return "", err + } + return federation.UUID, nil +} + +// DeleteFATEFederation deletes a FATE federation +func (app *FederationApp) DeleteFATEFederation(uuid string) error { + // XXX: this check should be placed in the domain level + instanceList, err := app.ParticipantFATERepo.ListByFederationUUID(uuid) + if err != nil { + return errors.Wrap(err, "failed to query federation participants") + } + participantList := instanceList.([]entity.ParticipantFATE) + if len(participantList) > 0 { + return errors.Errorf("cannot remove federation that still contains %v participants", len(participantList)) + } + return app.FederationFATERepo.DeleteByUUID(uuid) +} diff --git a/server/application/service/infra_provider_service.go b/server/application/service/infra_provider_service.go new file mode 100644 index 00000000..46610f64 --- /dev/null +++ b/server/application/service/infra_provider_service.go @@ -0,0 +1,214 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" +) + +// InfraProviderApp provide functions to manage the infra providers +type InfraProviderApp struct { + InfraProviderKubernetesRepo repo.InfraProviderRepository + EndpointKubeFATERepo repo.EndpointRepository +} + +// InfraProviderEditableItem contains properties of a provider that should be provided by the user +type InfraProviderEditableItem struct { + Name string `json:"name"` + Description string `json:"description"` + Type entity.InfraProviderType `json:"type"` +} + +// InfraProviderItemBase is the base data structure item +type InfraProviderItemBase struct { + InfraProviderEditableItem + UUID string `json:"uuid"` + CreatedAt time.Time `json:"created_at"` +} + +// InfraProviderListItem is an item for listing the providers +type InfraProviderListItem struct { + InfraProviderItemBase + KubernetesProviderInfo InfraProviderListItemKubernetes `json:"kubernetes_provider_info"` +} + +// InfraProviderListItemKubernetes contains list info related to a kubernetes infra provider +type InfraProviderListItemKubernetes struct { + APIServer string `json:"api_server"` +} + +// InfraProviderKubernetesConfig contains kubernetes provider details properties +type InfraProviderKubernetesConfig struct { + KubeConfig string `json:"kubeconfig_content"` + RegistryConfigFATE valueobject.KubeRegistryConfig `json:"registry_config_fate"` +} + +// InfraProviderCreationRequest represents a request to create an infra provider +type InfraProviderCreationRequest struct { + InfraProviderEditableItem + KubernetesProviderInfo InfraProviderKubernetesConfig `json:"kubernetes_provider_info"` +} + +// InfraProviderUpdateRequest represents a request to update an infra provider +type InfraProviderUpdateRequest struct { + InfraProviderEditableItem + KubernetesProviderInfo InfraProviderKubernetesConfig `json:"kubernetes_provider_info"` +} + +// InfraProviderInfoKubernetes contains information specific to kubernetes provider +type InfraProviderInfoKubernetes struct { + InfraProviderListItemKubernetes + InfraProviderKubernetesConfig +} + +// InfraProviderDetail contains details info of a provider +type InfraProviderDetail struct { + InfraProviderItemBase + KubernetesProviderInfo InfraProviderInfoKubernetes `json:"kubernetes_provider_info"` +} + +// GetProviderList returns provider list +func (app *InfraProviderApp) GetProviderList() ([]InfraProviderListItem, error) { + + var providerList []InfraProviderListItem + + domainProviderListInstance, err := app.InfraProviderKubernetesRepo.List() + if err != nil { + return nil, err + } + domainProviderList := domainProviderListInstance.([]entity.InfraProviderKubernetes) + for _, p := range domainProviderList { + APIServer, err := p.Config.APIHost() + if err != nil { + return nil, err + } + providerList = append(providerList, InfraProviderListItem{ + InfraProviderItemBase: InfraProviderItemBase{ + InfraProviderEditableItem: InfraProviderEditableItem{ + Name: p.Name, + Description: p.Description, + Type: p.Type, + }, + UUID: p.UUID, + CreatedAt: p.CreatedAt, + }, + KubernetesProviderInfo: InfraProviderListItemKubernetes{ + APIServer: APIServer, + }, + }) + } + return providerList, nil +} + +// TestKubernetesConnection validates the connection to the kubernetes cluster +func (app *InfraProviderApp) TestKubernetesConnection(kubeconfig *valueobject.KubeConfig) error { + return kubeconfig.Validate() +} + +// CreateProvider creates a provider +func (app *InfraProviderApp) CreateProvider(providerInfo *InfraProviderCreationRequest) error { + switch providerInfo.Type { + case entity.InfraProviderTypeK8s: + provider := &entity.InfraProviderKubernetes{ + InfraProviderBase: entity.InfraProviderBase{ + Name: providerInfo.Name, + Description: providerInfo.Description, + Type: providerInfo.Type, + }, + Config: valueobject.KubeConfig{ + KubeConfigContent: providerInfo.KubernetesProviderInfo.KubeConfig, + }, + RegistryConfigFATE: providerInfo.KubernetesProviderInfo.RegistryConfigFATE, + Repo: app.InfraProviderKubernetesRepo, + } + return provider.Create() + } + return errors.Errorf("unknown provider type: %s", providerInfo.Type) +} + +// DeleteProvider deletes a provider +func (app *InfraProviderApp) DeleteProvider(uuid string) error { + // TODO:this should be in the domain layer + endpointListInstance, err := app.EndpointKubeFATERepo.ListByInfraProviderUUID(uuid) + if err != nil { + return errors.Wrapf(err, "failed to query current infra's KubeFATE endpoint info") + } + domainEndpointList := endpointListInstance.([]entity.EndpointKubeFATE) + if len(domainEndpointList) != 0 { + return errors.Errorf("current infra provider %s still contains a KubeFATE endpoint", uuid) + } + return app.InfraProviderKubernetesRepo.DeleteByUUID(uuid) +} + +// GetProviderDetail returns detailed info of a provider +func (app *InfraProviderApp) GetProviderDetail(uuid string) (*InfraProviderDetail, error) { + domainProviderInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + domainProvider := domainProviderInstance.(*entity.InfraProviderKubernetes) + + apiHost, err := domainProvider.Config.APIHost() + if err != nil { + return nil, err + } + return &InfraProviderDetail{ + InfraProviderItemBase: InfraProviderItemBase{ + InfraProviderEditableItem: InfraProviderEditableItem{ + Name: domainProvider.Name, + Description: domainProvider.Description, + Type: domainProvider.Type, + }, + UUID: domainProvider.UUID, + CreatedAt: domainProvider.CreatedAt, + }, + KubernetesProviderInfo: InfraProviderInfoKubernetes{ + InfraProviderListItemKubernetes: InfraProviderListItemKubernetes{ + APIServer: apiHost, + }, + InfraProviderKubernetesConfig: InfraProviderKubernetesConfig{ + KubeConfig: domainProvider.Config.KubeConfigContent, + RegistryConfigFATE: domainProvider.RegistryConfigFATE, + }, + }, + }, nil +} + +// UpdateProvider changes provider settings +func (app *InfraProviderApp) UpdateProvider(uuid string, updateProviderInfo *InfraProviderUpdateRequest) error { + switch updateProviderInfo.Type { + case entity.InfraProviderTypeK8s: + provider := &entity.InfraProviderKubernetes{ + InfraProviderBase: entity.InfraProviderBase{ + UUID: uuid, + Name: updateProviderInfo.Name, + Description: updateProviderInfo.Description, + Type: updateProviderInfo.Type, + }, + Config: valueobject.KubeConfig{ + KubeConfigContent: updateProviderInfo.KubernetesProviderInfo.KubeConfig, + }, + RegistryConfigFATE: updateProviderInfo.KubernetesProviderInfo.RegistryConfigFATE, + Repo: app.InfraProviderKubernetesRepo, + } + return provider.Update() + } + return errors.Errorf("unknown provider type: %s", updateProviderInfo.Type) +} diff --git a/server/application/service/participant_openfl_service.go b/server/application/service/participant_openfl_service.go new file mode 100644 index 00000000..c090b2fb --- /dev/null +++ b/server/application/service/participant_openfl_service.go @@ -0,0 +1,277 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/service" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" +) + +// ParticipantOpenFLListItem contains basic info of an OpenFL participant +type ParticipantOpenFLListItem struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` + Type entity.ParticipantOpenFLType `json:"type"` + EndpointName string `json:"endpoint_name"` + EndpointUUID string `json:"endpoint_uuid"` + InfraProviderName string `json:"infra_provider_name"` + InfraProviderUUID string `json:"infra_provider_uuid"` + Namespace string `json:"namespace"` + ClusterUUID string `json:"cluster_uuid"` + Status entity.ParticipantOpenFLStatus `json:"status"` + AccessInfo entity.ParticipantOpenFLModulesAccessMap `json:"access_info"` + TokenStr string `json:"token_str"` + TokenName string `json:"token_name"` + Labels valueobject.Labels `json:"labels"` +} + +// ParticipantOpenFLListInFederation contains all the participants in an OpenFL federation +type ParticipantOpenFLListInFederation struct { + Director *ParticipantOpenFLListItem `json:"director"` + Envoy []*ParticipantOpenFLListItem `json:"envoy"` +} + +// OpenFLDirectorDetail contains detailed info of an OpenFL director +type OpenFLDirectorDetail struct { + ParticipantOpenFLListItem + ChartUUID string `json:"chart_uuid"` + DeploymentYAML string `json:"deployment_yaml"` + DirectorServerCertInfo entity.ParticipantComponentCertInfo `json:"director_server_cert_info"` + JupyterClientCertInfo entity.ParticipantComponentCertInfo `json:"jupyter_client_cert_info"` +} + +// OpenFLEnvoyDetail contains detailed info of an OpenFL envoy +type OpenFLEnvoyDetail struct { + ParticipantOpenFLListItem + ChartUUID string `json:"chart_uuid"` + EnvoyClientCertInfo entity.ParticipantComponentCertInfo `json:"envoy_client_cert_info"` +} + +func (app *ParticipantApp) getOpenFLDomainService() *service.ParticipantOpenFLService { + eventService := &service.EventService{ + EventRepo: app.EventRepo, + } + return &service.ParticipantOpenFLService{ + ParticipantOpenFLRepo: app.ParticipantOpenFLRepo, + TokenRepo: app.RegistrationTokenOpenFLRepo, + InfraRepo: app.InfraProviderKubernetesRepo, + ParticipantService: service.ParticipantService{ + FederationRepo: app.FederationOpenFLRepo, + ChartRepo: app.ChartRepo, + CertificateService: &service.CertificateService{ + CertificateAuthorityRepo: app.CertificateAuthorityRepo, + CertificateRepo: app.CertificateRepo, + CertificateBindingRepo: app.CertificateBindingRepo, + }, + EventService: eventService, + EndpointService: &service.EndpointService{ + InfraProviderKubernetesRepo: app.InfraProviderKubernetesRepo, + EndpointKubeFATERepo: app.EndpointKubeFATERepo, + ParticipantFATERepo: app.ParticipantFATERepo, + ParticipantOpenFLRepo: app.ParticipantOpenFLRepo, + EventService: eventService, + }, + }, + } +} + +// GetOpenFLDirectorDeploymentYAML returns the deployment yaml for an OpenFL director +func (app *ParticipantApp) GetOpenFLDirectorDeploymentYAML(req *service.ParticipantOpenFLDirectorYAMLCreationRequest) (string, error) { + return app.getOpenFLDomainService().GetOpenFLDirectorYAML(req) +} + +// CreateOpenFLDirector creates an OpenFL director using the specified endpoint +func (app *ParticipantApp) CreateOpenFLDirector(req *service.ParticipantOpenFLDirectorCreationRequest) (string, error) { + director, _, err := app.getOpenFLDomainService().CreateDirector(req) + if err != nil { + return "", err + } + return director.UUID, err +} + +// HandleOpenFLEnvoyRegistration handles registration request from an Envoy node +func (app *ParticipantApp) HandleOpenFLEnvoyRegistration(req *service.ParticipantOpenFLEnvoyRegistrationRequest) (string, error) { + envoy, err := app.getOpenFLDomainService().HandleRegistrationRequest(req) + if err != nil { + return "", err + } + return envoy.UUID, err +} + +// RemoveOpenFLDirector removes and uninstalls an OpenFL director deployment +func (app *ParticipantApp) RemoveOpenFLDirector(uuid string, force bool) error { + _, err := app.getOpenFLDomainService().RemoveDirector(uuid, force) + return err +} + +// RemoveOpenFLEnvoy removes and uninstalls an OpenFL envoy deployment +func (app *ParticipantApp) RemoveOpenFLEnvoy(uuid string, force bool) error { + return app.getOpenFLDomainService().RemoveEnvoy(uuid, force) +} + +// GetOpenFLParticipantList returns the current participants in an OpenFL federation +func (app *ParticipantApp) GetOpenFLParticipantList(federationUUID string) (*ParticipantOpenFLListInFederation, error) { + var participants ParticipantOpenFLListInFederation + instanceList, err := app.ParticipantOpenFLRepo.ListByFederationUUID(federationUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to list participant by federation") + } + domainParticipantList := instanceList.([]entity.ParticipantOpenFL) + + for _, domainParticipant := range domainParticipantList { + item := &ParticipantOpenFLListItem{ + UUID: domainParticipant.UUID, + Name: domainParticipant.Name, + Description: domainParticipant.Description, + CreatedAt: domainParticipant.CreatedAt, + Type: domainParticipant.Type, + EndpointName: "Unknown", + EndpointUUID: domainParticipant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + Namespace: domainParticipant.Namespace, + ClusterUUID: domainParticipant.ClusterUUID, + Status: domainParticipant.Status, + AccessInfo: domainParticipant.AccessInfo, + } + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(domainParticipant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + item.EndpointName = endpoint.Name + item.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + item.InfraProviderName = infra.Name + } + } + if domainParticipant.Type == entity.ParticipantOpenFLTypeDirector { + participants.Director = item + } else { + item.Labels = domainParticipant.Labels + item.TokenName = "Unknown" + item.TokenStr = "Unknown" + if instance, err := app.RegistrationTokenOpenFLRepo.GetByUUID(domainParticipant.TokenUUID); err == nil { + token := instance.(*entity.RegistrationTokenOpenFL) + item.TokenStr = token.Display() + item.TokenName = token.Name + } + participants.Envoy = append(participants.Envoy, item) + } + } + return &participants, nil +} + +// GetOpenFLDirectorDetail returns the detailed information of a OpenFL director +func (app *ParticipantApp) GetOpenFLDirectorDetail(uuid string) (*OpenFLDirectorDetail, error) { + participantInstance, err := app.ParticipantOpenFLRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + participant := participantInstance.(*entity.ParticipantOpenFL) + participantDetail := &OpenFLDirectorDetail{ + ParticipantOpenFLListItem: ParticipantOpenFLListItem{ + UUID: participant.UUID, + Name: participant.Name, + Description: participant.Description, + CreatedAt: participant.CreatedAt, + Type: participant.Type, + EndpointName: "Unknown", + EndpointUUID: participant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + Namespace: participant.Namespace, + ClusterUUID: participant.ClusterUUID, + Status: participant.Status, + AccessInfo: participant.AccessInfo, + }, + ChartUUID: participant.ChartUUID, + DeploymentYAML: participant.DeploymentYAML, + DirectorServerCertInfo: participant.CertConfig.DirectorServerCertInfo, + JupyterClientCertInfo: participant.CertConfig.JupyterClientCertInfo, + } + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(participant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + participantDetail.EndpointName = endpoint.Name + participantDetail.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + participantDetail.InfraProviderName = infra.Name + } + } + return participantDetail, nil +} + +// GetOpenFLEnvoyDetail returns the detailed information of a OpenFL envoy +func (app *ParticipantApp) GetOpenFLEnvoyDetail(uuid string) (*OpenFLEnvoyDetail, error) { + participantInstance, err := app.ParticipantOpenFLRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + participant := participantInstance.(*entity.ParticipantOpenFL) + participantDetail := &OpenFLEnvoyDetail{ + ParticipantOpenFLListItem: ParticipantOpenFLListItem{ + UUID: participant.UUID, + Name: participant.Name, + Description: participant.Description, + CreatedAt: participant.CreatedAt, + Type: participant.Type, + EndpointName: "Unknown", + EndpointUUID: participant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + Namespace: participant.Namespace, + ClusterUUID: participant.ClusterUUID, + Status: participant.Status, + AccessInfo: participant.AccessInfo, + TokenStr: "Unknown", + TokenName: "Unknown", + Labels: participant.Labels, + }, + ChartUUID: participant.ChartUUID, + EnvoyClientCertInfo: participant.CertConfig.EnvoyClientCertInfo, + } + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(participant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + participantDetail.EndpointName = endpoint.Name + participantDetail.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + participantDetail.InfraProviderName = infra.Name + } + } + if tokenInstance, err := app.RegistrationTokenOpenFLRepo.GetByUUID(participant.TokenUUID); err == nil { + token := tokenInstance.(*entity.RegistrationTokenOpenFL) + participantDetail.TokenStr = token.Display() + participantDetail.TokenName = token.Name + } + return participantDetail, nil +} + +// GetOpenFLEnvoyDetailWithTokenVerification returns the detailed information of a OpenFL envoy if the supplied token string is correct +func (app *ParticipantApp) GetOpenFLEnvoyDetailWithTokenVerification(uuid, tokenStr string) (*OpenFLEnvoyDetail, error) { + envoy, err := app.GetOpenFLEnvoyDetail(uuid) + if err != nil { + return nil, err + } + if envoy.TokenStr != tokenStr { + return nil, errors.New("invalid token") + } + return envoy, nil +} diff --git a/server/application/service/participant_service.go b/server/application/service/participant_service.go new file mode 100644 index 00000000..1881fb32 --- /dev/null +++ b/server/application/service/participant_service.go @@ -0,0 +1,307 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/service" + "github.com/pkg/errors" +) + +// ParticipantApp provide functions to manage the participants +type ParticipantApp struct { + ParticipantFATERepo repo.ParticipantFATERepository + FederationFATERepo repo.FederationRepository + + FederationOpenFLRepo repo.FederationRepository + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + RegistrationTokenOpenFLRepo repo.RegistrationTokenRepository + + EndpointKubeFATERepo repo.EndpointRepository + InfraProviderKubernetesRepo repo.InfraProviderRepository + ChartRepo repo.ChartRepository + CertificateAuthorityRepo repo.CertificateAuthorityRepository + CertificateRepo repo.CertificateRepository + CertificateBindingRepo repo.CertificateBindingRepository + EventRepo repo.EventRepository +} + +// ParticipantFATEListItem contains basic information of a FATE participant +type ParticipantFATEListItem struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` + Type entity.ParticipantFATEType `json:"type"` + EndpointName string `json:"endpoint_name"` + EndpointUUID string `json:"endpoint_uuid"` + InfraProviderName string `json:"infra_provider_name"` + InfraProviderUUID string `json:"infra_provider_uuid"` + ChartUUID string `json:"chart_uuid"` + Namespace string `json:"namespace"` + PartyID int `json:"party_id"` + ClusterUUID string `json:"cluster_uuid"` + Status entity.ParticipantFATEStatus `json:"status"` + AccessInfo entity.ParticipantFATEModulesAccessMap `json:"access_info"` + IsManaged bool `json:"is_managed"` +} + +// ParticipantFATEListInFederation has all the participants in a FATE federation +type ParticipantFATEListInFederation struct { + Exchange *ParticipantFATEListItem `json:"exchange"` + Clusters []*ParticipantFATEListItem `json:"clusters"` +} + +// FATEExchangeDetail contains the detailed info of a FATE exchange +type FATEExchangeDetail struct { + ParticipantFATEListItem + DeploymentYAML string `json:"deployment_yaml"` + ProxyServerCertInfo entity.ParticipantComponentCertInfo `json:"proxy_server_cert_info"` + FMLManagerServerCertInfo entity.ParticipantComponentCertInfo `json:"fml_manager_server_cert_info"` + FMLManagerClientCertInfo entity.ParticipantComponentCertInfo `json:"fml_manager_client_cert_info"` +} + +// FATEClusterDetail contains the detailed info a FATE cluster +type FATEClusterDetail struct { + ParticipantFATEListItem + DeploymentYAML string `json:"deployment_yaml"` + IngressInfo entity.ParticipantFATEIngressMap `json:"ingress_info"` + PulsarServerCertInfo entity.ParticipantComponentCertInfo `json:"pulsar_server_cert_info"` + SitePortalServerCertInfo entity.ParticipantComponentCertInfo `json:"site_portal_server_cert_info"` + SitePortalClientCertInfo entity.ParticipantComponentCertInfo `json:"site_portal_client_cert_info"` +} + +// CheckFATPartyID returns error if the current party id is taken in the specified federation +func (app *ParticipantApp) CheckFATPartyID(federationUUID string, partyID int) error { + return app.getFATEDomainService().CheckPartyIDConflict(federationUUID, partyID) +} + +// GetFATEExchangeDeploymentYAML returns the deployment yaml for a FATE exchange +func (app *ParticipantApp) GetFATEExchangeDeploymentYAML(req *service.ParticipantFATEExchangeYAMLCreationRequest) (string, error) { + return app.getFATEDomainService().GetExchangeDeploymentYAML(req) +} + +// GetFATEClusterDeploymentYAML returns the deployment yaml for a FATE cluster +func (app *ParticipantApp) GetFATEClusterDeploymentYAML(req *service.ParticipantFATEClusterYAMLCreationRequest) (string, error) { + return app.getFATEDomainService().GetClusterDeploymentYAML(req) +} + +// CreateFATEExchange creates a FATE exchange using the specified endpoint +func (app *ParticipantApp) CreateFATEExchange(req *service.ParticipantFATEExchangeCreationRequest) (string, error) { + exchange, _, err := app.getFATEDomainService().CreateExchange(req) + if err != nil { + return "", err + } + return exchange.UUID, err +} + +// CreateExternalFATEExchange creates an external FATE exchange +func (app *ParticipantApp) CreateExternalFATEExchange(req *service.ParticipantFATEExternalExchangeCreationRequest) (string, error) { + exchange, err := app.getFATEDomainService().CreateExternalExchange(req) + if err != nil { + return "", err + } + return exchange.UUID, err +} + +// CreateFATECluster creates a FATE cluster using the specified endpoint +func (app *ParticipantApp) CreateFATECluster(req *service.ParticipantFATEClusterCreationRequest) (string, error) { + cluster, _, err := app.getFATEDomainService().CreateCluster(req) + if err != nil { + return "", err + } + return cluster.UUID, err +} + +// CreateExternalFATECluster creates an external FATE cluster +func (app *ParticipantApp) CreateExternalFATECluster(req *service.ParticipantFATEExternalClusterCreationRequest) (string, error) { + cluster, _, err := app.getFATEDomainService().CreateExternalCluster(req) + if err != nil { + return "", err + } + return cluster.UUID, err +} + +// RemoveFATEExchange removes and uninstalls a FATE exchange deployment +func (app *ParticipantApp) RemoveFATEExchange(uuid string, force bool) error { + _, err := app.getFATEDomainService().RemoveExchange(uuid, force) + return err +} + +// RemoveFATECluster removes and uninstalls a FATE cluster deployment +func (app *ParticipantApp) RemoveFATECluster(uuid string, force bool) error { + _, err := app.getFATEDomainService().RemoveCluster(uuid, force) + return err +} + +func (app *ParticipantApp) getFATEDomainService() *service.ParticipantFATEService { + return &service.ParticipantFATEService{ + ParticipantFATERepo: app.ParticipantFATERepo, + ParticipantService: service.ParticipantService{ + FederationRepo: app.FederationFATERepo, + ChartRepo: app.ChartRepo, + CertificateService: &service.CertificateService{ + CertificateAuthorityRepo: app.CertificateAuthorityRepo, + CertificateRepo: app.CertificateRepo, + CertificateBindingRepo: app.CertificateBindingRepo, + }, + EventService: &service.EventService{ + EventRepo: app.EventRepo, + }, + EndpointService: &service.EndpointService{ + InfraProviderKubernetesRepo: app.InfraProviderKubernetesRepo, + EndpointKubeFATERepo: app.EndpointKubeFATERepo, + ParticipantFATERepo: app.ParticipantFATERepo, + }, + }, + } +} + +// GetFATEParticipantList returns the current participants in a FATE federation +func (app *ParticipantApp) GetFATEParticipantList(federationUUID string) (*ParticipantFATEListInFederation, error) { + var participants ParticipantFATEListInFederation + instanceList, err := app.ParticipantFATERepo.ListByFederationUUID(federationUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to list participant by federation") + } + domainParticipantList := instanceList.([]entity.ParticipantFATE) + + for _, domainParticipant := range domainParticipantList { + item := &ParticipantFATEListItem{ + UUID: domainParticipant.UUID, + Name: domainParticipant.Name, + Description: domainParticipant.Description, + CreatedAt: domainParticipant.CreatedAt, + Type: domainParticipant.Type, + EndpointName: "Unknown", + EndpointUUID: domainParticipant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + ChartUUID: domainParticipant.ChartUUID, + Namespace: domainParticipant.Namespace, + PartyID: domainParticipant.PartyID, + ClusterUUID: domainParticipant.ClusterUUID, + Status: domainParticipant.Status, + AccessInfo: domainParticipant.AccessInfo, + IsManaged: domainParticipant.IsManaged, + } + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(domainParticipant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + item.EndpointName = endpoint.Name + item.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + item.InfraProviderName = infra.Name + } + } + if domainParticipant.Type == entity.ParticipantFATETypeExchange { + participants.Exchange = item + } else { + participants.Clusters = append(participants.Clusters, item) + } + } + return &participants, nil +} + +// GetFATEExchangeDetail returns detailed info of a exchange +func (app *ParticipantApp) GetFATEExchangeDetail(uuid string) (*FATEExchangeDetail, error) { + participantInstance, err := app.ParticipantFATERepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + participant := participantInstance.(*entity.ParticipantFATE) + participantDetail := &FATEExchangeDetail{ + ParticipantFATEListItem: ParticipantFATEListItem{ + UUID: participant.UUID, + Name: participant.Name, + Description: participant.Description, + CreatedAt: participant.CreatedAt, + Type: participant.Type, + EndpointName: "Unknown", + EndpointUUID: participant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + ChartUUID: participant.ChartUUID, + Namespace: participant.Namespace, + PartyID: participant.PartyID, + ClusterUUID: participant.ClusterUUID, + Status: participant.Status, + AccessInfo: participant.AccessInfo, + IsManaged: participant.IsManaged, + }, + DeploymentYAML: participant.DeploymentYAML, + ProxyServerCertInfo: participant.CertConfig.ProxyServerCertInfo, + FMLManagerServerCertInfo: participant.CertConfig.FMLManagerServerCertInfo, + FMLManagerClientCertInfo: participant.CertConfig.FMLManagerClientCertInfo, + } + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(participant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + participantDetail.EndpointName = endpoint.Name + participantDetail.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + participantDetail.InfraProviderName = infra.Name + } + } + return participantDetail, nil +} + +// GetFATEClusterDetail returns detailed info of a exchange or cluster +func (app *ParticipantApp) GetFATEClusterDetail(uuid string) (*FATEClusterDetail, error) { + participantInstance, err := app.ParticipantFATERepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + participant := participantInstance.(*entity.ParticipantFATE) + participantDetail := &FATEClusterDetail{ + ParticipantFATEListItem: ParticipantFATEListItem{ + UUID: participant.UUID, + Name: participant.Name, + Description: participant.Description, + CreatedAt: participant.CreatedAt, + Type: participant.Type, + EndpointName: "Unknown", + EndpointUUID: participant.EndpointUUID, + InfraProviderName: "Unknown", + InfraProviderUUID: "Unknown", + ChartUUID: participant.ChartUUID, + Namespace: participant.Namespace, + PartyID: participant.PartyID, + ClusterUUID: participant.ClusterUUID, + Status: participant.Status, + AccessInfo: participant.AccessInfo, + IsManaged: participant.IsManaged, + }, + DeploymentYAML: participant.DeploymentYAML, + IngressInfo: participant.IngressInfo, + PulsarServerCertInfo: participant.CertConfig.PulsarServerCertInfo, + SitePortalServerCertInfo: participant.CertConfig.SitePortalServerCertInfo, + SitePortalClientCertInfo: participant.CertConfig.SitePortalClientCertInfo, + } + + if endpointInstance, err := app.EndpointKubeFATERepo.GetByUUID(participant.EndpointUUID); err == nil { + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + participantDetail.EndpointName = endpoint.Name + participantDetail.InfraProviderUUID = endpoint.InfraProviderUUID + if infraInstance, err := app.InfraProviderKubernetesRepo.GetByUUID(endpoint.InfraProviderUUID); err == nil { + infra := infraInstance.(*entity.InfraProviderKubernetes) + participantDetail.InfraProviderName = infra.Name + } + } + return participantDetail, nil +} diff --git a/server/application/service/user_service.go b/server/application/service/user_service.go new file mode 100644 index 00000000..dc7ba507 --- /dev/null +++ b/server/application/service/user_service.go @@ -0,0 +1,77 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/service" + "gorm.io/gorm" +) + +// UserApp provides user management service +type UserApp struct { + UserRepo repo.UserRepository +} + +// PublicUser represents a user info viewable to the public +type PublicUser struct { + Name string `json:"name"` + ID uint `json:"id"` + UUID string `json:"uuid"` +} + +// LoginInfo represents fields related with login +type LoginInfo struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// PwdChangeInfo represents fields related with login +type PwdChangeInfo struct { + CurPassword string `json:"cur_password"` + NewPassword string `json:"new_password"` +} + +// Login validates the loginInfo and returns a publicUser object on success +func (app *UserApp) Login(info *LoginInfo) (*PublicUser, error) { + loginService := &service.UserService{ + Repo: app.UserRepo, + } + user, err := loginService.LoginService(info.Username, info.Password) + if err != nil { + return nil, err + } + publicUser := PublicUser{ + Name: user.Name, + ID: user.ID, + UUID: user.UUID, + } + return &publicUser, nil +} + +// UpdateUserPassword changes user's password +func (app *UserApp) UpdateUserPassword(userId int, info *PwdChangeInfo) error { + user := &entity.User{ + Model: gorm.Model{ + ID: uint(userId), + }, + Repo: app.UserRepo, + } + if err := user.LoadById(); err != nil { + return err + } + return user.UpdatePwdInfo(info.CurPassword, info.NewPassword) +} diff --git a/server/constants/common.go b/server/constants/common.go new file mode 100644 index 00000000..2d91b9d8 --- /dev/null +++ b/server/constants/common.go @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +import "github.com/pkg/errors" + +const APIVersion = "v1" + +var ( + // Branch is the source branch + Branch string + + // Commit is the commit number + Commit string + + // BuildTime is the compiling time + BuildTime string +) + +var ErrNotImplemented = errors.New("not implemented") diff --git a/server/constants/response_code.go b/server/constants/response_code.go new file mode 100644 index 00000000..bad13506 --- /dev/null +++ b/server/constants/response_code.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +const ( + // RespNoErr means a success operation + RespNoErr = 0 + // RespInternalErr means some error occurred + RespInternalErr = 10001 +) diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 00000000..f8e4e478 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,5064 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/certificate": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Certificate" + ], + "summary": "Return issued certificate list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.CertificateListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Return certificate authority info", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.CertificateAuthorityDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Create a new certificate authority", + "parameters": [ + { + "description": "The CA information, currently for the type field only '1(StepCA)' is supported", + "name": "certificateAuthority", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CertificateAuthorityEditableItem" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority/built-in-ca": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Return the built-in certificate authority config", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.CertificateAuthorityConfigurationStepCA" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority/{uuid}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Updates the certificate authority", + "parameters": [ + { + "type": "string", + "description": "certificate authority UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The updated CA information", + "name": "certificateAuthority", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CertificateAuthorityEditableItem" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate/{uuid}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Certificate" + ], + "summary": "Delete the certificate which has no participant bindings", + "parameters": [ + { + "type": "string", + "description": "Certificate UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/chart": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Chart" + ], + "summary": "Return chart list, optionally with the specified type", + "parameters": [ + { + "type": "integer", + "description": "if set, it should be the chart type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ChartListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/chart/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Chart" + ], + "summary": "Get chart's detailed info", + "parameters": [ + { + "type": "string", + "description": "Chart UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ChartDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Return endpoints list data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EndpointListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Create a new endpoint by install a new one or add an existing one", + "parameters": [ + { + "description": "The endpoint information, currently for the type field only 'KubeFATE' is supported", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.EndpointCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the returned data contains the created endpoint", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/kubefate/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Get KubeFATE installation YAML content", + "parameters": [ + { + "type": "string", + "description": "username of the created KubeFATE service", + "name": "service_username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "password of the created KubeFATE service", + "name": "service_password", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "hostname domain name for the KubeFATE ingress object", + "name": "hostname", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "use_registry is to choose to use registry or not", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry is registry address", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "use_registry_secret is to choose to use registry secret or not", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_server_url is registry's server url", + "name": "registry_server_url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_username is registry's username", + "name": "registry_username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_password is registry's password", + "name": "registry_password", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/scan": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Scan the endpoints in an infra provider", + "parameters": [ + { + "description": "Provider UUID and endpoint type", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.EndpointScanRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EndpointListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Get endpoint's detailed info", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.EndpointDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Delete the endpoint", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, the endpoint installation will be removed too", + "name": "uninstall", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/{uuid}/kubefate/check": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Test connection to KubeFATE endpoint", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/event/{entity_uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Return event list of related entity", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EventListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Return federation list,", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.FederationListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE federation", + "parameters": [ + { + "description": "The federation info", + "name": "federation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FederationFATECreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created federation's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/cluster": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEClusterCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created cluster's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/cluster/external": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create an external FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExternalClusterCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created cluster's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/exchange": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExchangeCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created exchange's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/exchange/external": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create an external FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExternalExchangeCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created exchange's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/partyID/check": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Check if the party ID is available", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "party ID", + "name": "party_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/cluster/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get FATE cluster deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the federation uuid", + "name": "federation_uuid", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "party id", + "name": "party_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the FATE registry config saved in the infra provider", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the FATE registry secret saved in the infra provider", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "FATE registry config saved in the infra provider", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the persistent volume", + "name": "enable_persistence", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "provide the name of StorageClass", + "name": "storage_class", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/exchange/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get FATE exchange deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed 1: LoadBalancer 2: NodePort", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "FATE registry config saved in the infra provider", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry config", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry secret", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of a FATE federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FederationFATEDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/cluster/{clusterUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FATEClusterDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "cluster UUID", + "name": "clusterUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the cluster forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/exchange/{exchangeUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of FATE Exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FATEExchangeDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "exchange UUID", + "name": "exchangeUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the exchange forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get participant list of the specified federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ParticipantFATEListInFederation" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new OpenFL federation", + "parameters": [ + { + "description": "The federation info", + "name": "federation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FederationOpenFLCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created federation's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/director/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get OpenFL director deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the federation uuid", + "name": "federation_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed 1: LoadBalancer 2: NodePort", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "password to access the Jupyter Notebook", + "name": "jupyter_password", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "customized registry address", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry config", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry secret", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/envoy/register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Process Envoy registration request", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "registrationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantOpenFLEnvoyRegistrationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created director's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/envoy/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL envoy, by providing the envoy uuid and token string", + "parameters": [ + { + "type": "string", + "description": "envoy UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "token string", + "name": "token", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLEnvoyDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FederationOpenFLDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/director": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantOpenFLDirectorCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created director's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/director/{directorUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "director UUID", + "name": "directorUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLDirectorDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "director UUID", + "name": "directorUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the director forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/envoy/{envoyUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL envoy", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "envoy UUID", + "name": "envoyUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLEnvoyDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL envoy", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "envoy UUID", + "name": "envoyUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to envoy the director forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get participant list of the specified OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ParticipantOpenFLListInFederation" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/token": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get registration token list of the specified OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.RegistrationTokenOpenFLListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new registration token for an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The federation info", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RegistrationTokenOpenFLBasicInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/token/{tokenUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL federation registration token", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "token UUID", + "name": "tokenUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Return provider list data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.InfraProviderListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Create a new infra provider", + "parameters": [ + { + "description": "The provider information, currently for the type field only 'Kubernetes' is supported", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.InfraProviderCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra/kubernetes/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Test connection to a Kubernetes infra provider", + "parameters": [ + { + "description": "The kubeconfig content", + "name": "permission", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.KubeConfig" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Get infra provider's detailed info", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.InfraProviderDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Updates the infra provider", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The updated provider information", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.InfraProviderUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Delete the infra provider", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/current": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Return current user in the jwt token", + "responses": { + "200": { + "description": "Success, the name of current user", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "login to lifecycle manager", + "parameters": [ + { + "description": "credentials for login", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + } + } + } + }, + "/user/logout": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout from the lifecycle manager", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{userId}/password": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user password", + "parameters": [ + { + "description": "current and new password", + "name": "passwordChangeInfo", + "in": "body", + "schema": { + "$ref": "#/definitions/service.PwdChangeInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.CertificateAuthorityConfigurationStepCA": { + "type": "object", + "properties": { + "provisioner_name": { + "type": "string" + }, + "provisioner_password": { + "type": "string" + }, + "service_cert_pem": { + "type": "string" + }, + "service_url": { + "type": "string" + } + } + }, + "entity.EventData": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "log_level": { + "type": "string" + } + } + }, + "entity.ParticipantComponentCertInfo": { + "type": "object", + "properties": { + "binding_mode": { + "type": "integer" + }, + "common_name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ParticipantFATEIngress": { + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": { + "type": "string" + } + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "tls": { + "type": "boolean" + } + } + }, + "entity.ParticipantFATEIngressMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantFATEIngress" + } + }, + "entity.ParticipantFATEModulesAccessMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + }, + "entity.ParticipantModulesAccess": { + "type": "object", + "properties": { + "fqdn": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "service_type": { + "type": "string" + }, + "tls": { + "type": "boolean" + } + } + }, + "entity.ParticipantOpenFLModulesAccessMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + }, + "service.CertificateAuthorityDetail": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.CertificateAuthorityEditableItem": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + } + } + }, + "service.CertificateBindingListItem": { + "type": "object", + "properties": { + "federation_name": { + "type": "string" + }, + "federation_type": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "participant_name": { + "type": "string" + }, + "participant_type": { + "type": "string" + }, + "participant_uuid": { + "type": "string" + }, + "service_description": { + "type": "string" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.CertificateListItem": { + "type": "object", + "properties": { + "bindings": { + "type": "array", + "items": { + "$ref": "#/definitions/service.CertificateBindingListItem" + } + }, + "common_name": { + "type": "string" + }, + "expiration_date": { + "type": "string" + }, + "name": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ChartDetail": { + "type": "object", + "properties": { + "about": { + "type": "string" + }, + "chart_name": { + "type": "string" + }, + "contain_portal_services": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "values": { + "type": "string" + }, + "values_template": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.ChartListItem": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "contain_portal_services": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.EndpointCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "ingress_controller_service_mode": { + "type": "integer" + }, + "install": { + "type": "boolean" + }, + "kubefate_deployment_yaml": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.EndpointDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "kubefate_address": { + "type": "string" + }, + "kubefate_deployment_yaml": { + "type": "string" + }, + "kubefate_host": { + "type": "string" + }, + "kubefate_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.EndpointListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "kubefate_address": { + "type": "string" + }, + "kubefate_host": { + "type": "string" + }, + "kubefate_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.EndpointScanRequest": { + "type": "object", + "properties": { + "infra_provider_uuid": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.EventListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/entity.EventData" + }, + "entity_type": { + "type": "integer" + }, + "entity_uuid": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FATEClusterDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "ingress_info": { + "$ref": "#/definitions/entity.ParticipantFATEIngressMap" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "pulsar_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FATEExchangeDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "fml_manager_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "fml_manager_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "proxy_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationFATECreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.FederationFATEDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationOpenFLCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "shard_descriptor_config": { + "$ref": "#/definitions/valueobject.ShardDescriptorConfig" + }, + "use_customized_shard_descriptor": { + "type": "boolean" + } + } + }, + "service.FederationOpenFLDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "shard_descriptor_config": { + "$ref": "#/definitions/valueobject.ShardDescriptorConfig" + }, + "type": { + "type": "string" + }, + "use_customized_shard_descriptor": { + "type": "boolean" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderKubernetesConfig" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.InfraProviderDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderInfoKubernetes" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderInfoKubernetes": { + "type": "object", + "properties": { + "api_server": { + "type": "string" + }, + "kubeconfig_content": { + "type": "string" + }, + "registry_config_fate": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + } + } + }, + "service.InfraProviderKubernetesConfig": { + "type": "object", + "properties": { + "kubeconfig_content": { + "type": "string" + }, + "registry_config_fate": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + } + } + }, + "service.InfraProviderListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderListItemKubernetes" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderListItemKubernetes": { + "type": "object", + "properties": { + "api_server": { + "type": "string" + } + } + }, + "service.InfraProviderUpdateRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderKubernetesConfig" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.LoginInfo": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.OpenFLDirectorDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "director_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "jupyter_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.OpenFLEnvoyDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "envoy_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ParticipantFATEClusterCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_persistence": { + "type": "boolean" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "pulsar_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "registry_config": { + "description": "RegistrySecretConfig in valueobject.KubeRegistryConfig is not used for generating the yaml content", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + }, + "site_portal_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "storage_class": { + "type": "string" + } + } + }, + "service.ParticipantFATEExchangeCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "fml_manager_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "fml_manager_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "proxy_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "registry_config": { + "description": "RegistrySecretConfig in valueobject.KubeRegistryConfig is not used for generating the yaml content", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.ParticipantFATEExternalClusterCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nginx_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + }, + "party_id": { + "type": "integer" + }, + "pulsar_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + } + }, + "service.ParticipantFATEExternalExchangeCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nginx_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + }, + "traffic_server_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + } + }, + "service.ParticipantFATEListInFederation": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ParticipantFATEListItem" + } + }, + "exchange": { + "$ref": "#/definitions/service.ParticipantFATEListItem" + } + } + }, + "service.ParticipantFATEListItem": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ParticipantOpenFLDirectorCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "director_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "jupyter_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "jupyter_password": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry_config": { + "description": "for generating the yaml, RegistrySecretConfig is not used in RegistryConfig", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.ParticipantOpenFLEnvoyRegistrationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "config_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_psp": { + "type": "boolean" + }, + "kubeconfig": { + "description": "required", + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "description": "optional", + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry_config": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "skip_common_python_files": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "service.ParticipantOpenFLListInFederation": { + "type": "object", + "properties": { + "director": { + "$ref": "#/definitions/service.ParticipantOpenFLListItem" + }, + "envoy": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ParticipantOpenFLListItem" + } + } + } + }, + "service.ParticipantOpenFLListItem": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PwdChangeInfo": { + "type": "object", + "properties": { + "cur_password": { + "type": "string" + }, + "new_password": { + "type": "string" + } + } + }, + "service.RegistrationTokenOpenFLBasicInfo": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "expired_at": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "used": { + "type": "integer" + } + } + }, + "service.RegistrationTokenOpenFLListItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "expired_at": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "used": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "valueobject.KubeConfig": { + "type": "object", + "properties": { + "kubeconfig_content": { + "type": "string" + } + } + }, + "valueobject.KubeRegistryConfig": { + "type": "object", + "properties": { + "registry": { + "type": "string" + }, + "registry_secret_config": { + "$ref": "#/definitions/valueobject.KubeRegistrySecretConfig" + }, + "use_registry": { + "type": "boolean" + }, + "use_registry_secret": { + "type": "boolean" + } + } + }, + "valueobject.KubeRegistrySecretConfig": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "server_url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "valueobject.Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "valueobject.ShardDescriptorConfig": { + "type": "object", + "properties": { + "envoy_config_yaml": { + "type": "string" + }, + "python_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sample_shape": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_shape": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "lifecycle manager API service", + Description: "backend APIs of lifecycle manager service", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 00000000..341ddf1e --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,5040 @@ +{ + "swagger": "2.0", + "info": { + "description": "backend APIs of lifecycle manager service", + "title": "lifecycle manager API service", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "v1" + }, + "basePath": "/api/v1", + "paths": { + "/certificate": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Certificate" + ], + "summary": "Return issued certificate list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.CertificateListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Return certificate authority info", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.CertificateAuthorityDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Create a new certificate authority", + "parameters": [ + { + "description": "The CA information, currently for the type field only '1(StepCA)' is supported", + "name": "certificateAuthority", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CertificateAuthorityEditableItem" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority/built-in-ca": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Return the built-in certificate authority config", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.CertificateAuthorityConfigurationStepCA" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate-authority/{uuid}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "CertificateAuthority" + ], + "summary": "Updates the certificate authority", + "parameters": [ + { + "type": "string", + "description": "certificate authority UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The updated CA information", + "name": "certificateAuthority", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CertificateAuthorityEditableItem" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/certificate/{uuid}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Certificate" + ], + "summary": "Delete the certificate which has no participant bindings", + "parameters": [ + { + "type": "string", + "description": "Certificate UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/chart": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Chart" + ], + "summary": "Return chart list, optionally with the specified type", + "parameters": [ + { + "type": "integer", + "description": "if set, it should be the chart type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ChartListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/chart/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Chart" + ], + "summary": "Get chart's detailed info", + "parameters": [ + { + "type": "string", + "description": "Chart UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ChartDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Return endpoints list data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EndpointListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Create a new endpoint by install a new one or add an existing one", + "parameters": [ + { + "description": "The endpoint information, currently for the type field only 'KubeFATE' is supported", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.EndpointCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the returned data contains the created endpoint", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/kubefate/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Get KubeFATE installation YAML content", + "parameters": [ + { + "type": "string", + "description": "username of the created KubeFATE service", + "name": "service_username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "password of the created KubeFATE service", + "name": "service_password", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "hostname domain name for the KubeFATE ingress object", + "name": "hostname", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "use_registry is to choose to use registry or not", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry is registry address", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "use_registry_secret is to choose to use registry secret or not", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_server_url is registry's server url", + "name": "registry_server_url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_username is registry's username", + "name": "registry_username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "registry_password is registry's password", + "name": "registry_password", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/scan": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Scan the endpoints in an infra provider", + "parameters": [ + { + "description": "Provider UUID and endpoint type", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.EndpointScanRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EndpointListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Get endpoint's detailed info", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.EndpointDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Delete the endpoint", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, the endpoint installation will be removed too", + "name": "uninstall", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/endpoint/{uuid}/kubefate/check": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Endpoint" + ], + "summary": "Test connection to KubeFATE endpoint", + "parameters": [ + { + "type": "string", + "description": "Endpoint UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/event/{entity_uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Event" + ], + "summary": "Return event list of related entity", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.EventListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Return federation list,", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.FederationListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE federation", + "parameters": [ + { + "description": "The federation info", + "name": "federation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FederationFATECreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created federation's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/cluster": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEClusterCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created cluster's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/cluster/external": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create an external FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExternalClusterCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created cluster's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/exchange": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExchangeCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created exchange's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/exchange/external": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create an external FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantFATEExternalExchangeCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created exchange's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/:uuid/partyID/check": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Check if the party ID is available", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "party ID", + "name": "party_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/cluster/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get FATE cluster deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the federation uuid", + "name": "federation_uuid", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "party id", + "name": "party_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the FATE registry config saved in the infra provider", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the FATE registry secret saved in the infra provider", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "FATE registry config saved in the infra provider", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the persistent volume", + "name": "enable_persistence", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "provide the name of StorageClass", + "name": "storage_class", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/exchange/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get FATE exchange deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed 1: LoadBalancer 2: NodePort", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "FATE registry config saved in the infra provider", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry config", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry secret", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of a FATE federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FederationFATEDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/cluster/{clusterUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FATEClusterDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE cluster", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "cluster UUID", + "name": "clusterUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the cluster forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/exchange/{exchangeUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of FATE Exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FATEExchangeDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete a FATE exchange", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "exchange UUID", + "name": "exchangeUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the exchange forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/fate/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get participant list of the specified federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ParticipantFATEListInFederation" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new OpenFL federation", + "parameters": [ + { + "description": "The federation info", + "name": "federation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FederationOpenFLCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created federation's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/director/yaml": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get OpenFL director deployment yaml", + "parameters": [ + { + "type": "string", + "description": "the chart uuid", + "name": "chart_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "the federation uuid", + "name": "federation_uuid", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "name of the deployment", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "namespace of the deployment", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "type of the service to be exposed 1: LoadBalancer 2: NodePort", + "name": "service_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "password to access the Jupyter Notebook", + "name": "jupyter_password", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "customized registry address", + "name": "registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry config", + "name": "use_registry", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if use the customized registry secret", + "name": "use_registry_secret", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "choose if enable the podSecurityPolicy", + "name": "enable_psp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the yaml content", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/envoy/register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Process Envoy registration request", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "registrationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantOpenFLEnvoyRegistrationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created director's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/envoy/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL envoy, by providing the envoy uuid and token string", + "parameters": [ + { + "type": "string", + "description": "envoy UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "token string", + "name": "token", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLEnvoyDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.FederationOpenFLDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/director": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The creation requests", + "name": "creationRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ParticipantOpenFLDirectorCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success, the data field is the created director's uuid", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/director/{directorUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "director UUID", + "name": "directorUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLDirectorDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL director", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "director UUID", + "name": "directorUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to remove the director forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/envoy/{envoyUUID}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get specific info of OpenFL envoy", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "envoy UUID", + "name": "envoyUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.OpenFLEnvoyDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL envoy", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "envoy UUID", + "name": "envoyUUID", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, will try to envoy the director forcefully", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get participant list of the specified OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ParticipantOpenFLListInFederation" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/token": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Get registration token list of the specified OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.RegistrationTokenOpenFLListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Create a new registration token for an OpenFL federation", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The federation info", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RegistrationTokenOpenFLBasicInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/federation/openfl/{uuid}/token/{tokenUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Federation" + ], + "summary": "Delete an OpenFL federation registration token", + "parameters": [ + { + "type": "string", + "description": "federation UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "token UUID", + "name": "tokenUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Return provider list data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.InfraProviderListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Create a new infra provider", + "parameters": [ + { + "description": "The provider information, currently for the type field only 'Kubernetes' is supported", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.InfraProviderCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra/kubernetes/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Test connection to a Kubernetes infra provider", + "parameters": [ + { + "description": "The kubeconfig content", + "name": "permission", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.KubeConfig" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/infra/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Get infra provider's detailed info", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.InfraProviderDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Updates the infra provider", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "The updated provider information", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.InfraProviderUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "InfraProvider" + ], + "summary": "Delete the infra provider", + "parameters": [ + { + "type": "string", + "description": "Provider UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/current": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Return current user in the jwt token", + "responses": { + "200": { + "description": "Success, the name of current user", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "login to lifecycle manager", + "parameters": [ + { + "description": "credentials for login", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + } + } + } + }, + "/user/logout": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout from the lifecycle manager", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{userId}/password": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user password", + "parameters": [ + { + "description": "current and new password", + "name": "passwordChangeInfo", + "in": "body", + "schema": { + "$ref": "#/definitions/service.PwdChangeInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.CertificateAuthorityConfigurationStepCA": { + "type": "object", + "properties": { + "provisioner_name": { + "type": "string" + }, + "provisioner_password": { + "type": "string" + }, + "service_cert_pem": { + "type": "string" + }, + "service_url": { + "type": "string" + } + } + }, + "entity.EventData": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "log_level": { + "type": "string" + } + } + }, + "entity.ParticipantComponentCertInfo": { + "type": "object", + "properties": { + "binding_mode": { + "type": "integer" + }, + "common_name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ParticipantFATEIngress": { + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": { + "type": "string" + } + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "tls": { + "type": "boolean" + } + } + }, + "entity.ParticipantFATEIngressMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantFATEIngress" + } + }, + "entity.ParticipantFATEModulesAccessMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + }, + "entity.ParticipantModulesAccess": { + "type": "object", + "properties": { + "fqdn": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "service_type": { + "type": "string" + }, + "tls": { + "type": "boolean" + } + } + }, + "entity.ParticipantOpenFLModulesAccessMap": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + }, + "service.CertificateAuthorityDetail": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.CertificateAuthorityEditableItem": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + } + } + }, + "service.CertificateBindingListItem": { + "type": "object", + "properties": { + "federation_name": { + "type": "string" + }, + "federation_type": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "participant_name": { + "type": "string" + }, + "participant_type": { + "type": "string" + }, + "participant_uuid": { + "type": "string" + }, + "service_description": { + "type": "string" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.CertificateListItem": { + "type": "object", + "properties": { + "bindings": { + "type": "array", + "items": { + "$ref": "#/definitions/service.CertificateBindingListItem" + } + }, + "common_name": { + "type": "string" + }, + "expiration_date": { + "type": "string" + }, + "name": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ChartDetail": { + "type": "object", + "properties": { + "about": { + "type": "string" + }, + "chart_name": { + "type": "string" + }, + "contain_portal_services": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "values": { + "type": "string" + }, + "values_template": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.ChartListItem": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "contain_portal_services": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.EndpointCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "ingress_controller_service_mode": { + "type": "integer" + }, + "install": { + "type": "boolean" + }, + "kubefate_deployment_yaml": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.EndpointDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "kubefate_address": { + "type": "string" + }, + "kubefate_deployment_yaml": { + "type": "string" + }, + "kubefate_host": { + "type": "string" + }, + "kubefate_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.EndpointListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "kubefate_address": { + "type": "string" + }, + "kubefate_host": { + "type": "string" + }, + "kubefate_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.EndpointScanRequest": { + "type": "object", + "properties": { + "infra_provider_uuid": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.EventListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/entity.EventData" + }, + "entity_type": { + "type": "integer" + }, + "entity_uuid": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FATEClusterDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "ingress_info": { + "$ref": "#/definitions/entity.ParticipantFATEIngressMap" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "pulsar_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FATEExchangeDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "fml_manager_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "fml_manager_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "proxy_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationFATECreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.FederationFATEDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.FederationOpenFLCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "shard_descriptor_config": { + "$ref": "#/definitions/valueobject.ShardDescriptorConfig" + }, + "use_customized_shard_descriptor": { + "type": "boolean" + } + } + }, + "service.FederationOpenFLDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "name": { + "type": "string" + }, + "shard_descriptor_config": { + "$ref": "#/definitions/valueobject.ShardDescriptorConfig" + }, + "type": { + "type": "string" + }, + "use_customized_shard_descriptor": { + "type": "boolean" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderKubernetesConfig" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.InfraProviderDetail": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderInfoKubernetes" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderInfoKubernetes": { + "type": "object", + "properties": { + "api_server": { + "type": "string" + }, + "kubeconfig_content": { + "type": "string" + }, + "registry_config_fate": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + } + } + }, + "service.InfraProviderKubernetesConfig": { + "type": "object", + "properties": { + "kubeconfig_content": { + "type": "string" + }, + "registry_config_fate": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + } + } + }, + "service.InfraProviderListItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderListItemKubernetes" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.InfraProviderListItemKubernetes": { + "type": "object", + "properties": { + "api_server": { + "type": "string" + } + } + }, + "service.InfraProviderUpdateRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "kubernetes_provider_info": { + "$ref": "#/definitions/service.InfraProviderKubernetesConfig" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "service.LoginInfo": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.OpenFLDirectorDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "director_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "jupyter_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.OpenFLEnvoyDetail": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "envoy_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ParticipantFATEClusterCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_persistence": { + "type": "boolean" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "pulsar_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "registry_config": { + "description": "RegistrySecretConfig in valueobject.KubeRegistryConfig is not used for generating the yaml content", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + }, + "site_portal_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "site_portal_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "storage_class": { + "type": "string" + } + } + }, + "service.ParticipantFATEExchangeCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "fml_manager_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "fml_manager_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "proxy_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "registry_config": { + "description": "RegistrySecretConfig in valueobject.KubeRegistryConfig is not used for generating the yaml content", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.ParticipantFATEExternalClusterCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nginx_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + }, + "party_id": { + "type": "integer" + }, + "pulsar_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + } + }, + "service.ParticipantFATEExternalExchangeCreationRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nginx_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + }, + "traffic_server_access_info": { + "$ref": "#/definitions/entity.ParticipantModulesAccess" + } + } + }, + "service.ParticipantFATEListInFederation": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ParticipantFATEListItem" + } + }, + "exchange": { + "$ref": "#/definitions/service.ParticipantFATEListItem" + } + } + }, + "service.ParticipantFATEListItem": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantFATEModulesAccessMap" + }, + "chart_uuid": { + "type": "string" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "is_managed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ParticipantOpenFLDirectorCreationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "deployment_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "director_server_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "enable_psp": { + "type": "boolean" + }, + "endpoint_uuid": { + "type": "string" + }, + "federation_uuid": { + "type": "string" + }, + "jupyter_client_cert_info": { + "$ref": "#/definitions/entity.ParticipantComponentCertInfo" + }, + "jupyter_password": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry_config": { + "description": "for generating the yaml, RegistrySecretConfig is not used in RegistryConfig", + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "service_type": { + "type": "integer" + } + } + }, + "service.ParticipantOpenFLEnvoyRegistrationRequest": { + "type": "object", + "properties": { + "chart_uuid": { + "type": "string" + }, + "config_yaml": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enable_psp": { + "type": "boolean" + }, + "kubeconfig": { + "description": "required", + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "description": "optional", + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry_config": { + "$ref": "#/definitions/valueobject.KubeRegistryConfig" + }, + "skip_common_python_files": { + "type": "boolean" + }, + "token": { + "type": "string" + } + } + }, + "service.ParticipantOpenFLListInFederation": { + "type": "object", + "properties": { + "director": { + "$ref": "#/definitions/service.ParticipantOpenFLListItem" + }, + "envoy": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ParticipantOpenFLListItem" + } + } + } + }, + "service.ParticipantOpenFLListItem": { + "type": "object", + "properties": { + "access_info": { + "$ref": "#/definitions/entity.ParticipantOpenFLModulesAccessMap" + }, + "cluster_uuid": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "endpoint_name": { + "type": "string" + }, + "endpoint_uuid": { + "type": "string" + }, + "infra_provider_name": { + "type": "string" + }, + "infra_provider_uuid": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "token_name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PwdChangeInfo": { + "type": "object", + "properties": { + "cur_password": { + "type": "string" + }, + "new_password": { + "type": "string" + } + } + }, + "service.RegistrationTokenOpenFLBasicInfo": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "expired_at": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "used": { + "type": "integer" + } + } + }, + "service.RegistrationTokenOpenFLListItem": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "expired_at": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/valueobject.Labels" + }, + "limit": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "token_str": { + "type": "string" + }, + "used": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "valueobject.KubeConfig": { + "type": "object", + "properties": { + "kubeconfig_content": { + "type": "string" + } + } + }, + "valueobject.KubeRegistryConfig": { + "type": "object", + "properties": { + "registry": { + "type": "string" + }, + "registry_secret_config": { + "$ref": "#/definitions/valueobject.KubeRegistrySecretConfig" + }, + "use_registry": { + "type": "boolean" + }, + "use_registry_secret": { + "type": "boolean" + } + } + }, + "valueobject.KubeRegistrySecretConfig": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "server_url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "valueobject.Labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "valueobject.ShardDescriptorConfig": { + "type": "object", + "properties": { + "envoy_config_yaml": { + "type": "string" + }, + "python_files": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "sample_shape": { + "type": "array", + "items": { + "type": "string" + } + }, + "target_shape": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 00000000..c82609c5 --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,3080 @@ +basePath: /api/v1 +definitions: + api.GeneralResponse: + properties: + code: + example: 0 + type: integer + data: + type: object + message: + example: success + type: string + type: object + entity.CertificateAuthorityConfigurationStepCA: + properties: + provisioner_name: + type: string + provisioner_password: + type: string + service_cert_pem: + type: string + service_url: + type: string + type: object + entity.EventData: + properties: + description: + type: string + log_level: + type: string + type: object + entity.ParticipantComponentCertInfo: + properties: + binding_mode: + type: integer + common_name: + type: string + uuid: + type: string + type: object + entity.ParticipantFATEIngress: + properties: + addresses: + items: + type: string + type: array + hosts: + items: + type: string + type: array + tls: + type: boolean + type: object + entity.ParticipantFATEIngressMap: + additionalProperties: + $ref: '#/definitions/entity.ParticipantFATEIngress' + type: object + entity.ParticipantFATEModulesAccessMap: + additionalProperties: + $ref: '#/definitions/entity.ParticipantModulesAccess' + type: object + entity.ParticipantModulesAccess: + properties: + fqdn: + type: string + host: + type: string + port: + type: integer + service_type: + type: string + tls: + type: boolean + type: object + entity.ParticipantOpenFLModulesAccessMap: + additionalProperties: + $ref: '#/definitions/entity.ParticipantModulesAccess' + type: object + service.CertificateAuthorityDetail: + properties: + config: + additionalProperties: true + type: object + created_at: + type: string + description: + type: string + name: + type: string + status: + type: integer + status_message: + type: string + type: + type: integer + uuid: + type: string + type: object + service.CertificateAuthorityEditableItem: + properties: + config: + additionalProperties: true + type: object + description: + type: string + name: + type: string + type: + type: integer + type: object + service.CertificateBindingListItem: + properties: + federation_name: + type: string + federation_type: + type: string + federation_uuid: + type: string + participant_name: + type: string + participant_type: + type: string + participant_uuid: + type: string + service_description: + type: string + service_type: + type: integer + type: object + service.CertificateListItem: + properties: + bindings: + items: + $ref: '#/definitions/service.CertificateBindingListItem' + type: array + common_name: + type: string + expiration_date: + type: string + name: + type: string + serial_number: + type: string + uuid: + type: string + type: object + service.ChartDetail: + properties: + about: + type: string + chart_name: + type: string + contain_portal_services: + type: boolean + created_at: + type: string + description: + type: string + name: + type: string + type: + type: integer + uuid: + type: string + values: + type: string + values_template: + type: string + version: + type: string + type: object + service.ChartListItem: + properties: + chart_name: + type: string + contain_portal_services: + type: boolean + created_at: + type: string + description: + type: string + name: + type: string + type: + type: integer + uuid: + type: string + version: + type: string + type: object + service.EndpointCreationRequest: + properties: + description: + type: string + infra_provider_uuid: + type: string + ingress_controller_service_mode: + type: integer + install: + type: boolean + kubefate_deployment_yaml: + type: string + name: + type: string + type: + type: string + type: object + service.EndpointDetail: + properties: + created_at: + type: string + description: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + kubefate_address: + type: string + kubefate_deployment_yaml: + type: string + kubefate_host: + type: string + kubefate_version: + type: string + name: + type: string + status: + type: integer + type: + type: string + uuid: + type: string + type: object + service.EndpointListItem: + properties: + created_at: + type: string + description: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + kubefate_address: + type: string + kubefate_host: + type: string + kubefate_version: + type: string + name: + type: string + status: + type: integer + type: + type: string + uuid: + type: string + type: object + service.EndpointScanRequest: + properties: + infra_provider_uuid: + type: string + type: + type: string + type: object + service.EventListItem: + properties: + created_at: + type: string + data: + $ref: '#/definitions/entity.EventData' + entity_type: + type: integer + entity_uuid: + type: string + type: + type: integer + uuid: + type: string + type: object + service.FATEClusterDetail: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantFATEModulesAccessMap' + chart_uuid: + type: string + cluster_uuid: + type: string + created_at: + type: string + deployment_yaml: + type: string + description: + type: string + endpoint_name: + type: string + endpoint_uuid: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + ingress_info: + $ref: '#/definitions/entity.ParticipantFATEIngressMap' + is_managed: + type: boolean + name: + type: string + namespace: + type: string + party_id: + type: integer + pulsar_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + site_portal_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + site_portal_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + status: + type: integer + type: + type: integer + uuid: + type: string + type: object + service.FATEExchangeDetail: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantFATEModulesAccessMap' + chart_uuid: + type: string + cluster_uuid: + type: string + created_at: + type: string + deployment_yaml: + type: string + description: + type: string + endpoint_name: + type: string + endpoint_uuid: + type: string + fml_manager_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + fml_manager_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + infra_provider_name: + type: string + infra_provider_uuid: + type: string + is_managed: + type: boolean + name: + type: string + namespace: + type: string + party_id: + type: integer + proxy_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + status: + type: integer + type: + type: integer + uuid: + type: string + type: object + service.FederationFATECreationRequest: + properties: + description: + type: string + domain: + type: string + name: + type: string + type: object + service.FederationFATEDetail: + properties: + created_at: + type: string + description: + type: string + domain: + type: string + name: + type: string + type: + type: string + uuid: + type: string + type: object + service.FederationListItem: + properties: + created_at: + type: string + description: + type: string + name: + type: string + type: + type: string + uuid: + type: string + type: object + service.FederationOpenFLCreationRequest: + properties: + description: + type: string + domain: + type: string + name: + type: string + shard_descriptor_config: + $ref: '#/definitions/valueobject.ShardDescriptorConfig' + use_customized_shard_descriptor: + type: boolean + type: object + service.FederationOpenFLDetail: + properties: + created_at: + type: string + description: + type: string + domain: + type: string + name: + type: string + shard_descriptor_config: + $ref: '#/definitions/valueobject.ShardDescriptorConfig' + type: + type: string + use_customized_shard_descriptor: + type: boolean + uuid: + type: string + type: object + service.InfraProviderCreationRequest: + properties: + description: + type: string + kubernetes_provider_info: + $ref: '#/definitions/service.InfraProviderKubernetesConfig' + name: + type: string + type: + type: string + type: object + service.InfraProviderDetail: + properties: + created_at: + type: string + description: + type: string + kubernetes_provider_info: + $ref: '#/definitions/service.InfraProviderInfoKubernetes' + name: + type: string + type: + type: string + uuid: + type: string + type: object + service.InfraProviderInfoKubernetes: + properties: + api_server: + type: string + kubeconfig_content: + type: string + registry_config_fate: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + type: object + service.InfraProviderKubernetesConfig: + properties: + kubeconfig_content: + type: string + registry_config_fate: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + type: object + service.InfraProviderListItem: + properties: + created_at: + type: string + description: + type: string + kubernetes_provider_info: + $ref: '#/definitions/service.InfraProviderListItemKubernetes' + name: + type: string + type: + type: string + uuid: + type: string + type: object + service.InfraProviderListItemKubernetes: + properties: + api_server: + type: string + type: object + service.InfraProviderUpdateRequest: + properties: + description: + type: string + kubernetes_provider_info: + $ref: '#/definitions/service.InfraProviderKubernetesConfig' + name: + type: string + type: + type: string + type: object + service.LoginInfo: + properties: + password: + type: string + username: + type: string + type: object + service.OpenFLDirectorDetail: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantOpenFLModulesAccessMap' + chart_uuid: + type: string + cluster_uuid: + type: string + created_at: + type: string + deployment_yaml: + type: string + description: + type: string + director_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + endpoint_name: + type: string + endpoint_uuid: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + jupyter_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + labels: + $ref: '#/definitions/valueobject.Labels' + name: + type: string + namespace: + type: string + status: + type: integer + token_name: + type: string + token_str: + type: string + type: + type: integer + uuid: + type: string + type: object + service.OpenFLEnvoyDetail: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantOpenFLModulesAccessMap' + chart_uuid: + type: string + cluster_uuid: + type: string + created_at: + type: string + description: + type: string + endpoint_name: + type: string + endpoint_uuid: + type: string + envoy_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + infra_provider_name: + type: string + infra_provider_uuid: + type: string + labels: + $ref: '#/definitions/valueobject.Labels' + name: + type: string + namespace: + type: string + status: + type: integer + token_name: + type: string + token_str: + type: string + type: + type: integer + uuid: + type: string + type: object + service.ParticipantFATEClusterCreationRequest: + properties: + chart_uuid: + type: string + deployment_yaml: + type: string + description: + type: string + enable_persistence: + type: boolean + enable_psp: + type: boolean + endpoint_uuid: + type: string + federation_uuid: + type: string + name: + type: string + namespace: + type: string + party_id: + type: integer + pulsar_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + registry_config: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + description: RegistrySecretConfig in valueobject.KubeRegistryConfig is not + used for generating the yaml content + service_type: + type: integer + site_portal_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + site_portal_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + storage_class: + type: string + type: object + service.ParticipantFATEExchangeCreationRequest: + properties: + chart_uuid: + type: string + deployment_yaml: + type: string + description: + type: string + enable_psp: + type: boolean + endpoint_uuid: + type: string + federation_uuid: + type: string + fml_manager_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + fml_manager_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + name: + type: string + namespace: + type: string + proxy_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + registry_config: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + description: RegistrySecretConfig in valueobject.KubeRegistryConfig is not + used for generating the yaml content + service_type: + type: integer + type: object + service.ParticipantFATEExternalClusterCreationRequest: + properties: + description: + type: string + federation_uuid: + type: string + name: + type: string + nginx_access_info: + $ref: '#/definitions/entity.ParticipantModulesAccess' + party_id: + type: integer + pulsar_access_info: + $ref: '#/definitions/entity.ParticipantModulesAccess' + type: object + service.ParticipantFATEExternalExchangeCreationRequest: + properties: + description: + type: string + federation_uuid: + type: string + name: + type: string + nginx_access_info: + $ref: '#/definitions/entity.ParticipantModulesAccess' + traffic_server_access_info: + $ref: '#/definitions/entity.ParticipantModulesAccess' + type: object + service.ParticipantFATEListInFederation: + properties: + clusters: + items: + $ref: '#/definitions/service.ParticipantFATEListItem' + type: array + exchange: + $ref: '#/definitions/service.ParticipantFATEListItem' + type: object + service.ParticipantFATEListItem: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantFATEModulesAccessMap' + chart_uuid: + type: string + cluster_uuid: + type: string + created_at: + type: string + description: + type: string + endpoint_name: + type: string + endpoint_uuid: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + is_managed: + type: boolean + name: + type: string + namespace: + type: string + party_id: + type: integer + status: + type: integer + type: + type: integer + uuid: + type: string + type: object + service.ParticipantOpenFLDirectorCreationRequest: + properties: + chart_uuid: + type: string + deployment_yaml: + type: string + description: + type: string + director_server_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + enable_psp: + type: boolean + endpoint_uuid: + type: string + federation_uuid: + type: string + jupyter_client_cert_info: + $ref: '#/definitions/entity.ParticipantComponentCertInfo' + jupyter_password: + type: string + name: + type: string + namespace: + type: string + registry_config: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + description: for generating the yaml, RegistrySecretConfig is not used in + RegistryConfig + service_type: + type: integer + type: object + service.ParticipantOpenFLEnvoyRegistrationRequest: + properties: + chart_uuid: + type: string + config_yaml: + type: string + description: + type: string + enable_psp: + type: boolean + kubeconfig: + description: required + type: string + labels: + $ref: '#/definitions/valueobject.Labels' + name: + description: optional + type: string + namespace: + type: string + registry_config: + $ref: '#/definitions/valueobject.KubeRegistryConfig' + skip_common_python_files: + type: boolean + token: + type: string + type: object + service.ParticipantOpenFLListInFederation: + properties: + director: + $ref: '#/definitions/service.ParticipantOpenFLListItem' + envoy: + items: + $ref: '#/definitions/service.ParticipantOpenFLListItem' + type: array + type: object + service.ParticipantOpenFLListItem: + properties: + access_info: + $ref: '#/definitions/entity.ParticipantOpenFLModulesAccessMap' + cluster_uuid: + type: string + created_at: + type: string + description: + type: string + endpoint_name: + type: string + endpoint_uuid: + type: string + infra_provider_name: + type: string + infra_provider_uuid: + type: string + labels: + $ref: '#/definitions/valueobject.Labels' + name: + type: string + namespace: + type: string + status: + type: integer + token_name: + type: string + token_str: + type: string + type: + type: integer + uuid: + type: string + type: object + service.PwdChangeInfo: + properties: + cur_password: + type: string + new_password: + type: string + type: object + service.RegistrationTokenOpenFLBasicInfo: + properties: + description: + type: string + expired_at: + type: string + labels: + $ref: '#/definitions/valueobject.Labels' + limit: + type: integer + name: + type: string + used: + type: integer + type: object + service.RegistrationTokenOpenFLListItem: + properties: + description: + type: string + expired_at: + type: string + labels: + $ref: '#/definitions/valueobject.Labels' + limit: + type: integer + name: + type: string + token_str: + type: string + used: + type: integer + uuid: + type: string + type: object + valueobject.KubeConfig: + properties: + kubeconfig_content: + type: string + type: object + valueobject.KubeRegistryConfig: + properties: + registry: + type: string + registry_secret_config: + $ref: '#/definitions/valueobject.KubeRegistrySecretConfig' + use_registry: + type: boolean + use_registry_secret: + type: boolean + type: object + valueobject.KubeRegistrySecretConfig: + properties: + password: + type: string + server_url: + type: string + username: + type: string + type: object + valueobject.Labels: + additionalProperties: + type: string + type: object + valueobject.ShardDescriptorConfig: + properties: + envoy_config_yaml: + type: string + python_files: + additionalProperties: + type: string + type: object + sample_shape: + items: + type: string + type: array + target_shape: + items: + type: string + type: array + type: object +info: + contact: + name: FedLCM team + description: backend APIs of lifecycle manager service + termsOfService: http://swagger.io/terms/ + title: lifecycle manager API service + version: v1 +paths: + /certificate: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.CertificateListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return issued certificate list + tags: + - Certificate + /certificate-authority: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.CertificateAuthorityDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return certificate authority info + tags: + - CertificateAuthority + post: + parameters: + - description: The CA information, currently for the type field only '1(StepCA)' is supported + in: body + name: certificateAuthority + required: true + schema: + $ref: '#/definitions/service.CertificateAuthorityEditableItem' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new certificate authority + tags: + - CertificateAuthority + /certificate-authority/{uuid}: + put: + parameters: + - description: certificate authority UUID + in: path + name: uuid + required: true + type: string + - description: The updated CA information + in: body + name: certificateAuthority + required: true + schema: + $ref: '#/definitions/service.CertificateAuthorityEditableItem' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Updates the certificate authority + tags: + - CertificateAuthority + /certificate-authority/built-in-ca: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/entity.CertificateAuthorityConfigurationStepCA' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return the built-in certificate authority config + tags: + - CertificateAuthority + /certificate/{uuid}: + delete: + parameters: + - description: Certificate UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the certificate which has no participant bindings + tags: + - Certificate + /chart: + get: + parameters: + - description: if set, it should be the chart type + in: query + name: type + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ChartListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return chart list, optionally with the specified type + tags: + - Chart + /chart/{uuid}: + get: + parameters: + - description: Chart UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ChartDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get chart's detailed info + tags: + - Chart + /endpoint: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.EndpointListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return endpoints list data + tags: + - Endpoint + post: + parameters: + - description: The endpoint information, currently for the type field only 'KubeFATE' + is supported + in: body + name: provider + required: true + schema: + $ref: '#/definitions/service.EndpointCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the returned data contains the created endpoint + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new endpoint by install a new one or add an existing one + tags: + - Endpoint + /endpoint/{uuid}: + delete: + parameters: + - description: Endpoint UUID + in: path + name: uuid + required: true + type: string + - description: if set to true, the endpoint installation will be removed too + in: query + name: uninstall + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the endpoint + tags: + - Endpoint + get: + parameters: + - description: Endpoint UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.EndpointDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get endpoint's detailed info + tags: + - Endpoint + /endpoint/{uuid}/kubefate/check: + post: + parameters: + - description: Endpoint UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Test connection to KubeFATE endpoint + tags: + - Endpoint + /endpoint/kubefate/yaml: + get: + parameters: + - description: username of the created KubeFATE service + in: query + name: service_username + required: true + type: string + - description: password of the created KubeFATE service + in: query + name: service_password + required: true + type: string + - description: hostname domain name for the KubeFATE ingress object + in: query + name: hostname + required: true + type: string + - description: use_registry is to choose to use registry or not + in: query + name: use_registry + required: true + type: boolean + - description: registry is registry address + in: query + name: registry + required: true + type: string + - description: use_registry_secret is to choose to use registry secret or not + in: query + name: use_registry_secret + required: true + type: boolean + - description: registry_server_url is registry's server url + in: query + name: registry_server_url + required: true + type: string + - description: registry_username is registry's username + in: query + name: registry_username + required: true + type: string + - description: registry_password is registry's password + in: query + name: registry_password + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get KubeFATE installation YAML content + tags: + - Endpoint + /endpoint/scan: + post: + parameters: + - description: Provider UUID and endpoint type + in: body + name: provider + required: true + schema: + $ref: '#/definitions/service.EndpointScanRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.EndpointListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Scan the endpoints in an infra provider + tags: + - Endpoint + /event/{entity_uuid}: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.EventListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return event list of related entity + tags: + - Event + /federation: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.FederationListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return federation list, + tags: + - Federation + /federation/fate: + post: + parameters: + - description: The federation info + in: body + name: federation + required: true + schema: + $ref: '#/definitions/service.FederationFATECreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created federation's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new FATE federation + tags: + - Federation + /federation/fate/:uuid/cluster: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: creationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantFATEClusterCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created cluster's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new FATE cluster + tags: + - Federation + /federation/fate/:uuid/cluster/external: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: creationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantFATEExternalClusterCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created cluster's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create an external FATE cluster + tags: + - Federation + /federation/fate/:uuid/exchange: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: creationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantFATEExchangeCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created exchange's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new FATE exchange + tags: + - Federation + /federation/fate/:uuid/exchange/external: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: creationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantFATEExternalExchangeCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created exchange's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create an external FATE exchange + tags: + - Federation + /federation/fate/:uuid/partyID/check: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: party ID + in: query + name: party_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Check if the party ID is available + tags: + - Federation + /federation/fate/{uuid}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete a FATE federation + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.FederationFATEDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of a FATE federation + tags: + - Federation + /federation/fate/{uuid}/cluster/{clusterUUID}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: cluster UUID + in: path + name: clusterUUID + required: true + type: string + - description: if set to true, will try to remove the cluster forcefully + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete a FATE cluster + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.FATEClusterDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of FATE cluster + tags: + - Federation + /federation/fate/{uuid}/exchange/{exchangeUUID}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: exchange UUID + in: path + name: exchangeUUID + required: true + type: string + - description: if set to true, will try to remove the exchange forcefully + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete a FATE exchange + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.FATEExchangeDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of FATE Exchange + tags: + - Federation + /federation/fate/{uuid}/participant: + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ParticipantFATEListInFederation' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get participant list of the specified federation + tags: + - Federation + /federation/fate/cluster/yaml: + get: + parameters: + - description: the chart uuid + in: query + name: chart_uuid + required: true + type: string + - description: the federation uuid + in: query + name: federation_uuid + required: true + type: string + - description: party id + in: query + name: party_id + required: true + type: integer + - description: name of the deployment + in: query + name: name + required: true + type: string + - description: namespace of the deployment + in: query + name: namespace + required: true + type: string + - description: type of the service to be exposed + in: query + name: service_type + required: true + type: integer + - description: choose if use the FATE registry config saved in the infra provider + in: query + name: use_registry + required: true + type: boolean + - description: choose if use the FATE registry secret saved in the infra provider + in: query + name: use_registry_secret + required: true + type: boolean + - description: FATE registry config saved in the infra provider + in: query + name: registry + required: true + type: string + - description: choose if use the persistent volume + in: query + name: enable_persistence + required: true + type: boolean + - description: provide the name of StorageClass + in: query + name: storage_class + required: true + type: string + - description: choose if enable the podSecurityPolicy + in: query + name: enable_psp + required: true + type: boolean + produces: + - application/json + responses: + "200": + description: Success, the data field is the yaml content + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get FATE cluster deployment yaml + tags: + - Federation + /federation/fate/exchange/yaml: + get: + parameters: + - description: the chart uuid + in: query + name: chart_uuid + required: true + type: string + - description: name of the deployment + in: query + name: name + required: true + type: string + - description: namespace of the deployment + in: query + name: namespace + required: true + type: string + - description: 'type of the service to be exposed 1: LoadBalancer 2: NodePort' + in: query + name: service_type + required: true + type: integer + - description: FATE registry config saved in the infra provider + in: query + name: registry + required: true + type: string + - description: choose if use the customized registry config + in: query + name: use_registry + required: true + type: boolean + - description: choose if use the customized registry secret + in: query + name: use_registry_secret + required: true + type: boolean + - description: choose if enable the podSecurityPolicy + in: query + name: enable_psp + required: true + type: boolean + produces: + - application/json + responses: + "200": + description: Success, the data field is the yaml content + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get FATE exchange deployment yaml + tags: + - Federation + /federation/openfl: + post: + parameters: + - description: The federation info + in: body + name: federation + required: true + schema: + $ref: '#/definitions/service.FederationOpenFLCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created federation's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new OpenFL federation + tags: + - Federation + /federation/openfl/{uuid}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete an OpenFL federation + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.FederationOpenFLDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of an OpenFL federation + tags: + - Federation + /federation/openfl/{uuid}/director: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: creationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantOpenFLDirectorCreationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created director's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new OpenFL director + tags: + - Federation + /federation/openfl/{uuid}/director/{directorUUID}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: director UUID + in: path + name: directorUUID + required: true + type: string + - description: if set to true, will try to remove the director forcefully + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete an OpenFL director + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: director UUID + in: path + name: directorUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.OpenFLDirectorDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of OpenFL director + tags: + - Federation + /federation/openfl/{uuid}/envoy/{envoyUUID}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: envoy UUID + in: path + name: envoyUUID + required: true + type: string + - description: if set to true, will try to envoy the director forcefully + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete an OpenFL envoy + tags: + - Federation + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: envoy UUID + in: path + name: envoyUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.OpenFLEnvoyDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of OpenFL envoy + tags: + - Federation + /federation/openfl/{uuid}/participant: + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ParticipantOpenFLListInFederation' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get participant list of the specified OpenFL federation + tags: + - Federation + /federation/openfl/{uuid}/token: + get: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.RegistrationTokenOpenFLListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get registration token list of the specified OpenFL federation + tags: + - Federation + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The federation info + in: body + name: token + required: true + schema: + $ref: '#/definitions/service.RegistrationTokenOpenFLBasicInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new registration token for an OpenFL federation + tags: + - Federation + /federation/openfl/{uuid}/token/{tokenUUID}: + delete: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: token UUID + in: path + name: tokenUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete an OpenFL federation registration token + tags: + - Federation + /federation/openfl/director/yaml: + get: + parameters: + - description: the chart uuid + in: query + name: chart_uuid + required: true + type: string + - description: the federation uuid + in: query + name: federation_uuid + required: true + type: string + - description: name of the deployment + in: query + name: name + required: true + type: string + - description: namespace of the deployment + in: query + name: namespace + required: true + type: string + - description: 'type of the service to be exposed 1: LoadBalancer 2: NodePort' + in: query + name: service_type + required: true + type: integer + - description: password to access the Jupyter Notebook + in: query + name: jupyter_password + required: true + type: string + - description: customized registry address + in: query + name: registry + required: true + type: string + - description: choose if use the customized registry config + in: query + name: use_registry + required: true + type: boolean + - description: choose if use the customized registry secret + in: query + name: use_registry_secret + required: true + type: boolean + - description: choose if enable the podSecurityPolicy + in: query + name: enable_psp + required: true + type: boolean + produces: + - application/json + responses: + "200": + description: Success, the data field is the yaml content + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get OpenFL director deployment yaml + tags: + - Federation + /federation/openfl/envoy/{uuid}: + get: + parameters: + - description: envoy UUID + in: path + name: uuid + required: true + type: string + - description: token string + in: query + name: token + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.OpenFLEnvoyDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get specific info of OpenFL envoy, by providing the envoy uuid and + token string + tags: + - Federation + /federation/openfl/envoy/register: + post: + parameters: + - description: federation UUID + in: path + name: uuid + required: true + type: string + - description: The creation requests + in: body + name: registrationRequest + required: true + schema: + $ref: '#/definitions/service.ParticipantOpenFLEnvoyRegistrationRequest' + produces: + - application/json + responses: + "200": + description: Success, the data field is the created director's uuid + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process Envoy registration request + tags: + - Federation + /infra: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.InfraProviderListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return provider list data + tags: + - InfraProvider + post: + parameters: + - description: The provider information, currently for the type field only 'Kubernetes' + is supported + in: body + name: provider + required: true + schema: + $ref: '#/definitions/service.InfraProviderCreationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new infra provider + tags: + - InfraProvider + /infra/{uuid}: + delete: + parameters: + - description: Provider UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the infra provider + tags: + - InfraProvider + get: + parameters: + - description: Provider UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.InfraProviderDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get infra provider's detailed info + tags: + - InfraProvider + put: + parameters: + - description: Provider UUID + in: path + name: uuid + required: true + type: string + - description: The updated provider information + in: body + name: provider + required: true + schema: + $ref: '#/definitions/service.InfraProviderUpdateRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Updates the infra provider + tags: + - InfraProvider + /infra/kubernetes/connect: + post: + parameters: + - description: The kubeconfig content + in: body + name: permission + required: true + schema: + $ref: '#/definitions/valueobject.KubeConfig' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Test connection to a Kubernetes infra provider + tags: + - InfraProvider + /user/{userId}/password: + put: + parameters: + - description: current and new password + in: body + name: passwordChangeInfo + schema: + $ref: '#/definitions/service.PwdChangeInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Update user password + tags: + - User + /user/current: + get: + produces: + - application/json + responses: + "200": + description: Success, the name of current user + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return current user in the jwt token + tags: + - User + /user/login: + post: + parameters: + - description: credentials for login + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/service.LoginInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + summary: login to lifecycle manager + tags: + - User + /user/logout: + post: + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: logout from the lifecycle manager + tags: + - User +swagger: "2.0" diff --git a/server/domain/entity/certificate.go b/server/domain/entity/certificate.go new file mode 100644 index 00000000..e120d04f --- /dev/null +++ b/server/domain/entity/certificate.go @@ -0,0 +1,91 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "crypto/x509" + "encoding/pem" + "gorm.io/gorm" +) + +// Certificate is the certificate managed by this service +type Certificate struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + SerialNumberStr string `gorm:"type:varchar(255)"` + PEM string `gorm:"type:text"` + ChainPEM string `gorm:"type:text"` + Chain []*x509.Certificate `gorm:"-"` + *x509.Certificate `gorm:"-"` +} + +func (c *Certificate) BeforeSave(tx *gorm.DB) error { + c.SerialNumberStr = c.SerialNumber.String() + _, err := c.EncodePEM() + return err +} + +func (c *Certificate) AfterFind(tx *gorm.DB) error { + b, _ := pem.Decode([]byte(c.PEM)) + certificate, err := x509.ParseCertificate(b.Bytes) + if err != nil { + return err + } + c.Certificate = certificate + c.Chain = nil + rest := []byte(c.ChainPEM) + var block *pem.Block + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type == "CERTIFICATE" { + chainCert, err := x509.ParseCertificate(b.Bytes) + if err != nil { + return err + } + c.Chain = append(c.Chain, chainCert) + } + if len(rest) == 0 { + break + } + } + return nil +} + +// EncodePEM saves the PEM content in the related fields and returns the complete PEM content +func (c *Certificate) EncodePEM() ([]byte, error) { + if c.PEM == "" { + c.PEM = string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: c.Raw, + })) + } + certBytes := []byte(c.PEM) + if c.ChainPEM == "" && len(c.Chain) > 0 { + for _, chainCert := range c.Chain { + c.ChainPEM += string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: chainCert.Raw, + })) + } + } + certBytes = append(certBytes, c.ChainPEM...) + return certBytes, nil +} diff --git a/server/domain/entity/certificate_authority.go b/server/domain/entity/certificate_authority.go new file mode 100644 index 00000000..5d202352 --- /dev/null +++ b/server/domain/entity/certificate_authority.go @@ -0,0 +1,96 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/pkg/errors" + stepcaapiv1 "github.com/smallstep/certificates/cas/apiv1" + "github.com/smallstep/certificates/cas/stepcas" + "gorm.io/gorm" +) + +// CertificateAuthorityClient provides interface functions to work with a CA service +type CertificateAuthorityClient interface { + stepcaapiv1.CertificateAuthorityService +} + +// CertificateAuthority represent a certificate authority +type CertificateAuthority struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `gorm:"type:text"` + Type CertificateAuthorityType + ConfigurationJSON string `gorm:"type:text;column:config_json"` +} + +// CertificateAuthorityType is the certificate authority type +type CertificateAuthorityType uint8 + +const ( + CertificateAuthorityTypeUnknown CertificateAuthorityType = iota + CertificateAuthorityTypeStepCA +) + +// CertificateAuthorityConfigurationStepCA contains basic configuration for a StepCA service +type CertificateAuthorityConfigurationStepCA struct { + ServiceURL string `json:"service_url" mapstructure:"service_url"` + ServiceCertificatePEM string `json:"service_cert_pem" mapstructure:"service_cert_pem"` + ProvisionerName string `json:"provisioner_name" mapstructure:"provisioner_name"` + ProvisionerPassword string `json:"provisioner_password" mapstructure:"provisioner_password"` +} + +// Client returns a client to work with a CA service, with basic validation executed +func (ca *CertificateAuthority) Client() (CertificateAuthorityClient, error) { + switch ca.Type { + case CertificateAuthorityTypeStepCA: + var config CertificateAuthorityConfigurationStepCA + err := json.Unmarshal([]byte(ca.ConfigurationJSON), &config) + if err != nil { + return nil, err + } + b, _ := pem.Decode([]byte(config.ServiceCertificatePEM)) + if b == nil { + return nil, errors.Errorf("failed to decode PEM block") + } + return stepcas.New(context.TODO(), stepcaapiv1.Options{ + CertificateAuthority: config.ServiceURL, + CertificateAuthorityFingerprint: fmt.Sprintf("%x", sha256.Sum256(b.Bytes)), + CertificateIssuer: &stepcaapiv1.CertificateIssuer{ + Type: "jwk", + Provisioner: config.ProvisionerName, + Password: config.ProvisionerPassword, + }, + }) + } + return nil, errors.Errorf("unknown CA type: %v", ca.Type) +} + +func (ca *CertificateAuthority) RootCert() (*x509.Certificate, error) { + var config CertificateAuthorityConfigurationStepCA + err := json.Unmarshal([]byte(ca.ConfigurationJSON), &config) + if err != nil { + return nil, err + } + b, _ := pem.Decode([]byte(config.ServiceCertificatePEM)) + return x509.ParseCertificate(b.Bytes) +} diff --git a/server/domain/entity/certificate_binding.go b/server/domain/entity/certificate_binding.go new file mode 100644 index 00000000..f9e9f0f3 --- /dev/null +++ b/server/domain/entity/certificate_binding.go @@ -0,0 +1,69 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// CertificateBinding is the binding relationship between certificate and the service +type CertificateBinding struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + CertificateUUID string `gorm:"type:varchar(36);not null"` + ParticipantUUID string `gorm:"type:varchar(36);not null"` + ServiceType CertificateBindingServiceType + FederationUUID string `gorm:"type:varchar(36)"` + FederationType FederationType `gorm:"type:varchar(255)"` +} + +type CertificateBindingServiceType uint8 + +const ( + CertificateBindingServiceTypeUnknown CertificateBindingServiceType = iota + CertificateBindingServiceTypeATS + CertificateBindingServiceTypePulsarServer + CertificateBindingServiceFMLManagerServer + CertificateBindingServiceFMLManagerClient + CertificateBindingServiceSitePortalServer + CertificateBindingServiceSitePortalClient +) + +// openfl +const ( + CertificateBindingServiceTypeOpenFLDirector CertificateBindingServiceType = iota + 101 + CertificateBindingServiceTypeOpenFLJupyter + CertificateBindingServiceTypeOpenFLEnvoy +) + +func (t CertificateBindingServiceType) String() string { + switch t { + case CertificateBindingServiceTypeATS: + return "pulsar proxy" + case CertificateBindingServiceTypePulsarServer: + return "pulsar server" + case CertificateBindingServiceFMLManagerServer: + return "fml manager server" + case CertificateBindingServiceFMLManagerClient: + return "fml manager client" + case CertificateBindingServiceSitePortalServer: + return "site portal server" + case CertificateBindingServiceSitePortalClient: + return "site portal client" + case CertificateBindingServiceTypeOpenFLDirector: + return "openfl director" + case CertificateBindingServiceTypeOpenFLJupyter: + return "openfl jupyter client" + } + return "unknown" +} diff --git a/server/domain/entity/chart.go b/server/domain/entity/chart.go new file mode 100644 index 00000000..834a7a51 --- /dev/null +++ b/server/domain/entity/chart.go @@ -0,0 +1,48 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "gorm.io/gorm" +) + +// Chart is the helm chart +type Chart struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `gorm:"type:text"` + Type ChartType + ChartName string `gorm:"type:varchar(255)"` + Version string `gorm:"type:varchar(32);not null"` + AppVersion string `gorm:"type:varchar(32);not null"` + Chart string `gorm:"type:text;not null"` + InitialYamlTemplate string `gorm:"type:text;not null"` + Values string `gorm:"type:text;not null"` + ValuesTemplate string `gorm:"type:text;not null"` + ArchiveContent []byte `gorm:"type:mediumblob"` + Private bool +} + +// ChartType is the supported deployment type +type ChartType uint8 + +const ( + ChartTypeUnknown ChartType = iota + ChartTypeFATEExchange + ChartTypeFATECluster + ChartTypeOpenFLDirector + ChartTypeOpenFLEnvoy +) diff --git a/server/domain/entity/endpoint.go b/server/domain/entity/endpoint.go new file mode 100644 index 00000000..d744753a --- /dev/null +++ b/server/domain/entity/endpoint.go @@ -0,0 +1,66 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// EndpointBase contains common information of an endpoint +type EndpointBase struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36);index;unique"` + InfraProviderUUID string `gorm:"type:varchar(36)"` + Name string `json:"name" gorm:"type:varchar(255);unique;not null"` + Description string `json:"description" gorm:"type:text"` + Version string `json:"version" gorm:"type:varchar(255)"` + Type EndpointType `gorm:"type:varchar(255)"` + Status EndpointStatus +} + +// EndpointType is the enum types of the provider +type EndpointType string + +const ( + EndpointTypeUnknown EndpointType = "Unknown" + EndpointTypeKubeFATE EndpointType = "KubeFATE" +) + +// EndpointStatus is the status of the endpoint +type EndpointStatus uint8 + +const ( + EndpointStatusUnknown EndpointStatus = iota + EndpointStatusCreating + EndpointStatusReady + EndpointStatusDismissed + EndpointStatusUnavailable + EndpointStatusDeleting +) + +func (s EndpointStatus) String() string { + res := "Unknown" + switch s { + case EndpointStatusCreating: + res = "Creating" + case EndpointStatusReady: + res = "Ready" + case EndpointStatusDismissed: + res = "Dismissed" + case EndpointStatusUnavailable: + res = "Unavailable" + case EndpointStatusDeleting: + res = "Deleting" + } + return res +} diff --git a/server/domain/entity/endpoint_kubefate.go b/server/domain/entity/endpoint_kubefate.go new file mode 100644 index 00000000..614b78ef --- /dev/null +++ b/server/domain/entity/endpoint_kubefate.go @@ -0,0 +1,56 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "database/sql/driver" + "encoding/json" +) + +// EndpointKubeFATE is an KubeFATE type of endpoint +type EndpointKubeFATE struct { + EndpointBase + Config KubeFATEConfig `gorm:"type:text"` + DeploymentYAML string `gorm:"type:text"` + IngressControllerYAML string `gorm:"type:text"` +} + +// KubeFATEConfig records basic info of a KubeFATE config +type KubeFATEConfig struct { + IngressAddress string `json:"ingress_address"` + IngressRuleHost string `json:"ingress_rule_host"` + UsePortForwarding bool `json:"use_port_forwarding"` +} + +func (c KubeFATEConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *KubeFATEConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// EndpointKubeFATEIngressControllerServiceMode is the service mode of the ingress controller +type EndpointKubeFATEIngressControllerServiceMode uint8 + +const ( + // EndpointKubeFATEIngressControllerServiceModeSkip means there is an ingress controller in the infra, and we skip installing it by ourselves + EndpointKubeFATEIngressControllerServiceModeSkip EndpointKubeFATEIngressControllerServiceMode = iota + EndpointKubeFATEIngressControllerServiceModeLoadBalancer + EndpointKubeFATEIngressControllerServiceModeModeNodePort + // EndpointKubeFATEIngressControllerServiceModeModeNonexistent means there is no ingress controller at all, we will use other method to access the KubeFATE service + EndpointKubeFATEIngressControllerServiceModeModeNonexistent +) diff --git a/server/domain/entity/event.go b/server/domain/entity/event.go new file mode 100644 index 00000000..dcd3de81 --- /dev/null +++ b/server/domain/entity/event.go @@ -0,0 +1,92 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "gorm.io/gorm" +) + +// Event records events related to a certain entity +type Event struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Type EventType + EntityUUID string `gorm:"type:varchar(36);column:entity_uuid"` + EntityType EntityType + Data string `gorm:"type:text" ` +} + +// EventType is the supported Event type +type EventType uint8 + +const ( + EventTypeUnknown EventType = iota + EventTypeLogMessage +) + +// EventData is detail info of an event +type EventData struct { + Description string `json:"description"` + LogLevel string `json:"log_level"` +} + +// EventLogLevel is the level of the log event +type EventLogLevel uint8 + +const ( + EventLogLevelUnknown EventLogLevel = iota + EventLogLevelInfo + EventLogLevelError +) + +// EntityType is the entity which records events +type EntityType uint8 + +const ( + EntityTypeUnknown EntityType = iota + EntityTypeEndpoint + EntityTypeExchange + EntityTypeCluster +) + +// openfl +const ( + EntityTypeOpenFLDirector EntityType = iota + 101 + EntityTypeOpenFLEnvoy +) + +func (t EventLogLevel) String() string { + switch t { + case EventLogLevelInfo: + return "Info" + case EventLogLevelError: + return "Error" + } + return "Unknown" +} + +func (t EntityType) String() string { + switch t { + case EntityTypeEndpoint: + return "Endpoint" + case EntityTypeExchange: + return "Exchange" + case EntityTypeCluster: + return "Cluster" + case EntityTypeOpenFLDirector: + return "OpenFL Director" + } + return "Unknown" +} diff --git a/server/domain/entity/federation.go b/server/domain/entity/federation.go new file mode 100644 index 00000000..9572682b --- /dev/null +++ b/server/domain/entity/federation.go @@ -0,0 +1,38 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/server/domain/repo" + "gorm.io/gorm" +) + +// Federation is a logic concept that contains multiple FML participants +type Federation struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `gorm:"type:text"` + Type FederationType `gorm:"type:varchar(255);not null"` + Repo repo.FederationRepository `gorm:"-"` +} + +// FederationType is the type of federation +type FederationType string + +const ( + FederationTypeFATE FederationType = "FATE" + FederationTypeOpenFL FederationType = "OpenFL" +) diff --git a/server/domain/entity/federation_fate.go b/server/domain/entity/federation_fate.go new file mode 100644 index 00000000..a9b8f880 --- /dev/null +++ b/server/domain/entity/federation_fate.go @@ -0,0 +1,35 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/server/domain/utils" + "github.com/pkg/errors" +) + +// FederationFATE represents a FATE federation +type FederationFATE struct { + Federation + Domain string `gorm:"type:varchar(255);not null"` +} + +// Create creates the FATE federation record in the repo +func (federation *FederationFATE) Create() error { + federation.Type = FederationTypeFATE + if !utils.IsDomainName(federation.Domain) { + return errors.New("invalid domain name") + } + return federation.Repo.Create(federation) +} diff --git a/server/domain/entity/federation_openfl.go b/server/domain/entity/federation_openfl.go new file mode 100644 index 00000000..75478b10 --- /dev/null +++ b/server/domain/entity/federation_openfl.go @@ -0,0 +1,52 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "strings" + + "github.com/FederatedAI/FedLCM/server/domain/utils" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" +) + +// FederationOpenFL represents an OpenFL federation +type FederationOpenFL struct { + Federation + Domain string `gorm:"type:varchar(255);not null"` + UseCustomizedShardDescriptor bool + ShardDescriptorConfig *valueobject.ShardDescriptorConfig `gorm:"type:text"` +} + +// Create creates the OpenFL federation record in the repo +func (federation *FederationOpenFL) Create() error { + federation.Type = FederationTypeOpenFL + if !utils.IsDomainName(federation.Domain) { + return errors.New("invalid domain name") + } + if federation.UseCustomizedShardDescriptor { + for file, _ := range federation.ShardDescriptorConfig.PythonFiles { + if strings.Contains(file, " ") { + return errors.New("filename cannot contain space") + } + } + } + return federation.Repo.Create(federation) +} + +func (FederationOpenFL) TableName() string { + // just following the gorm convention + return "federation_openfls" +} diff --git a/server/domain/entity/infra_provider.go b/server/domain/entity/infra_provider.go new file mode 100644 index 00000000..2642aa26 --- /dev/null +++ b/server/domain/entity/infra_provider.go @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "gorm.io/gorm" +) + +// InfraProviderBase contains common information of an infra provider +type InfraProviderBase struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36);index;unique"` + Name string `json:"name" gorm:"type:varchar(255);unique;not null"` + Description string `json:"description" gorm:"type:text"` + Type InfraProviderType `gorm:"type:varchar(255)"` +} + +// InfraProviderType is the enum types of the provider +type InfraProviderType string + +const ( + InfraProviderTypeUnknown InfraProviderType = "Unknown" + InfraProviderTypeK8s InfraProviderType = "Kubernetes" +) diff --git a/server/domain/entity/infra_provider_kubernetes.go b/server/domain/entity/infra_provider_kubernetes.go new file mode 100644 index 00000000..ec81d113 --- /dev/null +++ b/server/domain/entity/infra_provider_kubernetes.go @@ -0,0 +1,84 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/utils" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// InfraProviderKubernetes is a provider that is using a kubernetes cluster +type InfraProviderKubernetes struct { + InfraProviderBase + Config valueobject.KubeConfig `json:"config" gorm:"type:text"` + ConfigSHA256 string `json:"config_sha_256" gorm:"type:varchar(64)"` + APIHost string `json:"api_host" gorm:"type:varchar(256)"` + RegistryConfigFATE valueobject.KubeRegistryConfig `json:"registry_config_fate" gorm:"type:text"` + // TODO: add server version? + Repo repo.InfraProviderRepository `json:"-" gorm:"-"` +} + +// Validate checks if the necessary information is provided correctly +func (p *InfraProviderKubernetes) Validate() error { + return p.Config.Validate() +} + +// Create checks the config and saves the object to the repo +func (p *InfraProviderKubernetes) Create() error { + if err := p.Validate(); err != nil { + return err + } + p.UUID = uuid.NewV4().String() + if err := p.Repo.Create(p); err != nil { + return err + } + return nil +} + +// Update checks the config and update the object to the repo +func (p *InfraProviderKubernetes) Update() error { + if err := p.Validate(); err != nil { + return err + } + if err := p.Repo.UpdateByUUID(p); err != nil { + return err + } + return nil +} + +func (p *InfraProviderKubernetes) BeforeSave(tx *gorm.DB) error { + // encrypted registry secret password + encryptedSecret, err := utils.Encrypt(p.RegistryConfigFATE.RegistrySecretConfig.Password) + if err != nil { + return err + } + p.RegistryConfigFATE.RegistrySecretConfig.Password = encryptedSecret + p.APIHost, _ = p.Config.APIHost() + p.ConfigSHA256 = p.Config.SHA2565() + return nil +} + +func (p *InfraProviderKubernetes) AfterFind(tx *gorm.DB) error { + // decrypted registry secret password + decryptedSecret, err := utils.Decrypt(p.RegistryConfigFATE.RegistrySecretConfig.Password) + if err != nil { + return err + } + p.RegistryConfigFATE.RegistrySecretConfig.Password = decryptedSecret + return nil +} diff --git a/server/domain/entity/participant.go b/server/domain/entity/participant.go new file mode 100644 index 00000000..efea49ce --- /dev/null +++ b/server/domain/entity/participant.go @@ -0,0 +1,102 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "database/sql/driver" + "encoding/json" + + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" +) + +// Participant represent a federation participant +type Participant struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `gorm:"type:text"` + FederationUUID string `gorm:"type:varchar(36)"` + EndpointUUID string `gorm:"type:varchar(36)"` + ChartUUID string `gorm:"type:varchar(36)"` + Namespace string `gorm:"type:varchar(255)"` + ClusterUUID string `gorm:"type:varchar(36)"` + JobUUID string `gorm:"type:varchar(36)"` + DeploymentYAML string `gorm:"type:text"` + IsManaged bool `gorm:"type:bool"` + ExtraAttribute ParticipantExtraAttribute `gorm:"type:text"` +} + +// ParticipantExtraAttribute record some extra attributes of the participant +type ParticipantExtraAttribute struct { + IsNewNamespace bool `json:"is_new_namespace"` + UseRegistrySecret bool `json:"use_registry_secret"` +} + +func (a ParticipantExtraAttribute) Value() (driver.Value, error) { + bJson, err := json.Marshal(a) + return bJson, err +} + +func (a *ParticipantExtraAttribute) Scan(v interface{}) error { + // ignore any errors + _ = json.Unmarshal([]byte(v.(string)), a) + return nil +} + +// ParticipantDefaultServiceType is the default service type of the exposed services in the participant +type ParticipantDefaultServiceType uint8 + +const ( + ParticipantDefaultServiceTypeUnknown ParticipantDefaultServiceType = iota + ParticipantDefaultServiceTypeLoadBalancer + ParticipantDefaultServiceTypeNodePort +) + +func (t ParticipantDefaultServiceType) String() string { + switch t { + case ParticipantDefaultServiceTypeNodePort: + return "NodePort" + case ParticipantDefaultServiceTypeLoadBalancer: + return "LoadBalancer" + } + return "Unknown" +} + +// ParticipantComponentCertInfo contains certificate information of a component in participant +type ParticipantComponentCertInfo struct { + BindingMode ParticipantCertBindingMode `json:"binding_mode"` + UUID string `json:"uuid"` + CommonName string `json:"common_name"` +} + +// ParticipantCertBindingMode is the certificate binding mode +type ParticipantCertBindingMode uint8 + +const ( + CertBindingModeUnknown ParticipantCertBindingMode = iota + CertBindingModeSkip + CertBindingModeReuse + CertBindingModeCreate +) + +// ParticipantModulesAccess contains access info of a participant service +type ParticipantModulesAccess struct { + ServiceType corev1.ServiceType `json:"service_type"` + Host string `json:"host"` + Port int `json:"port"` + TLS bool `json:"tls"` + FQDN string `json:"fqdn"` +} diff --git a/server/domain/entity/participant_fate.go b/server/domain/entity/participant_fate.go new file mode 100644 index 00000000..2260ed79 --- /dev/null +++ b/server/domain/entity/participant_fate.go @@ -0,0 +1,146 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "database/sql/driver" + "encoding/json" +) + +// ParticipantFATE represent a FATE type participant +type ParticipantFATE struct { + Participant + PartyID int + Type ParticipantFATEType + Status ParticipantFATEStatus + CertConfig ParticipantFATECertConfig `gorm:"type:text"` + AccessInfo ParticipantFATEModulesAccessMap `gorm:"type:text"` + IngressInfo ParticipantFATEIngressMap `gorm:"type:text"` +} + +// ParticipantFATECertConfig contains all the certificate configuration of a FATE participant +type ParticipantFATECertConfig struct { + ProxyServerCertInfo ParticipantComponentCertInfo `json:"proxy_server_cert_info"` + FMLManagerServerCertInfo ParticipantComponentCertInfo `json:"fml_manager_server_cert_info"` + FMLManagerClientCertInfo ParticipantComponentCertInfo `json:"fml_manager_client_cert_info"` + + PulsarServerCertInfo ParticipantComponentCertInfo `json:"pulsar_server_cert_info"` + SitePortalServerCertInfo ParticipantComponentCertInfo `json:"site_portal_server_cert_info"` + SitePortalClientCertInfo ParticipantComponentCertInfo `json:"site_portal_client_cert_info"` +} + +func (c ParticipantFATECertConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ParticipantFATECertConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// ParticipantFATEServiceName is all the exposed FATE participant service name +type ParticipantFATEServiceName string + +const ( + ParticipantFATEServiceNameNginx ParticipantFATEServiceName = "nginx" + ParticipantFATEServiceNameATS ParticipantFATEServiceName = "traffic-server" + ParticipantFATEServiceNamePulsar ParticipantFATEServiceName = "pulsar-public-tls" + ParticipantFATEServiceNamePortal ParticipantFATEServiceName = "frontend" + ParticipantFATEServiceNameFMLMgr ParticipantFATEServiceName = "fml-manager-server" +) + +const ( + ParticipantFATESecretNameATS = "traffic-server-cert" + ParticipantFATESecretNamePulsar = "pulsar-cert" + ParticipantFATESecretNameFMLMgr = "fml-manager-cert" + ParticipantFATESecretNamePortal = "site-portal-cert" +) + +// ParticipantFATEModulesAccessMap contains the exposed services access information +type ParticipantFATEModulesAccessMap map[ParticipantFATEServiceName]ParticipantModulesAccess + +func (c ParticipantFATEModulesAccessMap) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ParticipantFATEModulesAccessMap) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// ParticipantFATEType is the participant type +type ParticipantFATEType uint8 + +const ( + ParticipantFATETypeUnknown ParticipantFATEType = iota + ParticipantFATETypeExchange + ParticipantFATETypeCluster +) + +func (t ParticipantFATEType) String() string { + switch t { + case ParticipantFATETypeExchange: + return "exchange" + case ParticipantFATETypeCluster: + return "cluster" + } + return "unknown" +} + +// ParticipantFATEStatus is the status of the fate participant +type ParticipantFATEStatus uint8 + +const ( + ParticipantFATEStatusUnknown ParticipantFATEStatus = iota + ParticipantFATEStatusActive + ParticipantFATEStatusInstalling + ParticipantFATEStatusRemoving + ParticipantFATEStatusReconfiguring + ParticipantFATEStatusFailed +) + +func (t ParticipantFATEStatus) String() string { + switch t { + case ParticipantFATEStatusActive: + return "Active" + case ParticipantFATEStatusInstalling: + return "Installing" + case ParticipantFATEStatusRemoving: + return "Removing" + case ParticipantFATEStatusReconfiguring: + return "Reconfiguring" + case ParticipantFATEStatusFailed: + return "Failed" + } + return "Unknown" +} + +type ParticipantFATEIngressMap map[string]ParticipantFATEIngress + +func (c ParticipantFATEIngressMap) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ParticipantFATEIngressMap) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// ParticipantFATEIngress contains ingress info of a FATE participant module +type ParticipantFATEIngress struct { + Hosts []string `json:"hosts"` + Addresses []string `json:"addresses"` + TLS bool `json:"tls"` +} diff --git a/server/domain/entity/participant_openfl.go b/server/domain/entity/participant_openfl.go new file mode 100644 index 00000000..f9eb972e --- /dev/null +++ b/server/domain/entity/participant_openfl.go @@ -0,0 +1,131 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "database/sql/driver" + "encoding/json" + + "github.com/FederatedAI/FedLCM/server/domain/valueobject" +) + +const ( + ParticipantOpenFLSecretNameDirector = "director-cert" + ParticipantOpenFLSecretNameJupyter = "notebook-cert" + ParticipantOpenFLSecretNameEnvoy = "envoy-cert" +) + +// ParticipantOpenFL represent an OpenFL type participant +type ParticipantOpenFL struct { + Participant + Type ParticipantOpenFLType + Status ParticipantOpenFLStatus + InfraUUID string `gorm:"type:varchar(36)"` + TokenUUID string `gorm:"type:varchar(36)"` + CertConfig ParticipantOpenFLCertConfig `gorm:"type:text"` + AccessInfo ParticipantOpenFLModulesAccessMap `gorm:"type:text"` + Labels valueobject.Labels `gorm:"type:text"` +} + +// ParticipantOpenFLType is the openfl participant type +type ParticipantOpenFLType uint8 + +const ( + ParticipantOpenFLTypeUnknown ParticipantOpenFLType = iota + ParticipantOpenFLTypeDirector + ParticipantOpenFLTypeEnvoy +) + +func (t ParticipantOpenFLType) String() string { + switch t { + case ParticipantOpenFLTypeDirector: + return "director" + case ParticipantOpenFLTypeEnvoy: + return "envoy" + } + return "unknown" +} + +// ParticipantOpenFLStatus is the status of the openfl participant +type ParticipantOpenFLStatus uint8 + +const ( + ParticipantOpenFLStatusUnknown ParticipantOpenFLStatus = iota + ParticipantOpenFLStatusActive + ParticipantOpenFLStatusRemoving + ParticipantOpenFLStatusFailed + ParticipantOpenFLStatusInstallingDirector + ParticipantOpenFLStatusConfiguringInfra + ParticipantOpenFLStatusInstallingEndpoint + ParticipantOpenFLStatusInstallingEnvoy +) + +func (t ParticipantOpenFLStatus) String() string { + switch t { + case ParticipantOpenFLStatusActive: + return "Active" + case ParticipantOpenFLStatusRemoving: + return "Removing" + case ParticipantOpenFLStatusFailed: + return "Failed" + case ParticipantOpenFLStatusInstallingDirector: + return "Installing Director" + case ParticipantOpenFLStatusConfiguringInfra: + return "Configuring Infrastructure" + case ParticipantOpenFLStatusInstallingEndpoint: + return "Installing Endpoint" + case ParticipantOpenFLStatusInstallingEnvoy: + return "Installing Envoy" + } + return "Unknown" +} + +// ParticipantOpenFLCertConfig contains configurations for certificates in an OpenFL participant +type ParticipantOpenFLCertConfig struct { + DirectorServerCertInfo ParticipantComponentCertInfo `json:"director_server_cert_info"` + JupyterClientCertInfo ParticipantComponentCertInfo `json:"jupyter_client_cert_info"` + + EnvoyClientCertInfo ParticipantComponentCertInfo `json:"envoy_client_cert_info"` +} + +func (c ParticipantOpenFLCertConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ParticipantOpenFLCertConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// ParticipantOpenFLServiceName is a enum of the exposed service names +type ParticipantOpenFLServiceName string + +const ( + ParticipantOpenFLServiceNameDirector ParticipantOpenFLServiceName = "director" + ParticipantOpenFLServiceNameAggregator ParticipantOpenFLServiceName = "agg" + ParticipantOpenFLServiceNameJupyter ParticipantOpenFLServiceName = "notebook" +) + +// ParticipantOpenFLModulesAccessMap contains the exposed services in an OpenFL participant +type ParticipantOpenFLModulesAccessMap map[ParticipantOpenFLServiceName]ParticipantModulesAccess + +func (c ParticipantOpenFLModulesAccessMap) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ParticipantOpenFLModulesAccessMap) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} diff --git a/server/domain/entity/token_openfl.go b/server/domain/entity/token_openfl.go new file mode 100644 index 00000000..022b96f8 --- /dev/null +++ b/server/domain/entity/token_openfl.go @@ -0,0 +1,160 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "math/rand" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +var ( + alphanumericLetters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + tokenTypeMap = map[string]RegistrationTokenType{ + "rand16": RegistrationTokenTypeRand16, + } +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// RegistrationTokenParse pares a complete token string and returns the token type and the token string +func RegistrationTokenParse(tokenDisplayedStr string) (RegistrationTokenType, string, error) { + splitStr := strings.SplitN(tokenDisplayedStr, ":", 2) + if len(splitStr) == 2 { + if t, ok := tokenTypeMap[splitStr[0]]; ok { + return t, splitStr[1], nil + } + } + return RegistrationTokenTypeUnknown, "", errors.Errorf("unknown token: %s", tokenDisplayedStr) +} + +// RegistrationToken is the token entity a participant can use to register itself +type RegistrationToken struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + Name string `gorm:"type:varchar(255);not null"` + Description string `gorm:"type:text"` + TokenType RegistrationTokenType + TokenStr string `gorm:"type:varchar(255)"` + Repo repo.RegistrationTokenRepository `gorm:"-"` +} + +// RegistrationTokenType is an enum of token types +type RegistrationTokenType uint8 + +const ( + RegistrationTokenTypeUnknown RegistrationTokenType = iota + RegistrationTokenTypeRand16 +) + +// RegistrationTokenOpenFL contains extra information for a token used in OpenFL federations +type RegistrationTokenOpenFL struct { + RegistrationToken + FederationUUID string `gorm:"type:varchar(36)"` + ExpiredAt time.Time + Limit int + Labels valueobject.Labels `gorm:"type:text"` + ParticipantRepo repo.ParticipantOpenFLRepository `gorm:"-"` +} + +// Create creates the OpenFL federation record in the repo +func (token *RegistrationTokenOpenFL) Create() error { + if token.Name == "" { + return errors.New("missing name") + } + if token.FederationUUID == "" { + return errors.New("missing federation UUID") + } + if token.ExpiredAt.Before(time.Now()) { + return errors.New("invalid expiration time") + } + if token.Limit <= 0 { + return errors.New("invalid limit number") + } + if token.UUID == "" { + token.UUID = uuid.NewV4().String() + } + if token.TokenType == RegistrationTokenTypeUnknown { + token.TokenType = RegistrationTokenTypeRand16 + } + if token.TokenStr == "" { + token.TokenStr = token.TokenType.Generate() + } + return token.Repo.Create(token) +} + +// Display returns a string representing the token and its type +func (token *RegistrationTokenOpenFL) Display() string { + if token.TokenStr != "" { + return token.TokenType.DisplayStr() + ":" + token.TokenStr + } + return "" +} + +// Validate returns whether this token is still valid +func (token *RegistrationTokenOpenFL) Validate() error { + if token.TokenStr == "" { + return errors.New("empty token string") + } + if time.Now().After(token.ExpiredAt) { + return errors.New("token expired") + } + if token.ParticipantRepo == nil { + return errors.New("nil participant repo") + } + if count, err := token.ParticipantRepo.CountByTokenUUID(token.UUID); err != nil { + return errors.Wrap(err, "failed to query token count") + } else if count >= token.Limit { + return errors.New("token limit reached") + } + return nil +} + +// Generate generates the token string based on the token type +func (t RegistrationTokenType) Generate() string { + switch t { + case RegistrationTokenTypeRand16: + b := make([]rune, 16) + for i := range b { + b[i] = alphanumericLetters[rand.Intn(len(alphanumericLetters))] + } + return string(b) + default: + panic("unknown RegistrationTokenType") + } +} + +// DisplayStr returns a string representing the type +func (t RegistrationTokenType) DisplayStr() string { + switch t { + case RegistrationTokenTypeRand16: + return "rand16" + default: + return "unknown" + } +} + +func (RegistrationTokenOpenFL) TableName() string { + // just following the gorm convention + return "registration_token_openfls" +} diff --git a/server/domain/entity/user.go b/server/domain/entity/user.go new file mode 100644 index 00000000..e7f4a725 --- /dev/null +++ b/server/domain/entity/user.go @@ -0,0 +1,78 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "regexp" + "strings" + + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// User is a representation of the user available in the lifecycle manager +type User struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + // Name is the user's name + Name string `gorm:"type:varchar(255);unique;not null"` + // Password is the user's hashed password + Password string `gorm:"type:varchar(255)"` + // Repo is the repository to persistent related data + Repo repo.UserRepository `gorm:"-"` +} + +// LoadById reads the info from the repo +func (u *User) LoadById() error { + return u.Repo.LoadById(u) +} + +// UpdatePwdInfo changes a users password +func (u *User) UpdatePwdInfo(curPassword, newPassword string) error { + //Check the input of current password is matching to record + if err := func() error { + if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(curPassword)); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + //Check new password is valid + if strings.TrimSpace(newPassword) == "" { + return errors.Errorf("new password can not be empty") + } + if curPassword == newPassword { + return errors.Errorf("new password can not be same to the current password") + } + if len(newPassword) < 8 || len(newPassword) > 20 { + return errors.Errorf("new password should be 8-20 characters long") + } + var hasUpperCase = regexp.MustCompile(`[A-Z]`).MatchString + var hasLowerCase = regexp.MustCompile(`[a-z]`).MatchString + var hasNumbers = regexp.MustCompile(`[0-9]`).MatchString + + if !hasUpperCase(newPassword) || !hasLowerCase(newPassword) || !hasNumbers(newPassword) { + return errors.Errorf("password should be with at least 1 uppercase, 1 lowercase and 1 number") + } + //hash new password + hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + return u.Repo.UpdatePasswordById(u.ID, string(hashedNewPassword)) +} diff --git a/server/domain/repo/certificate_authority_repo.go b/server/domain/repo/certificate_authority_repo.go new file mode 100644 index 00000000..fd84fc0e --- /dev/null +++ b/server/domain/repo/certificate_authority_repo.go @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// CertificateAuthorityRepository is the interface to handle certificate authority's persistence related actions +type CertificateAuthorityRepository interface { + // Create takes an *entity.CertificateAuthority and creates a record in the repository + Create(interface{}) error + // UpdateByUUID takes an *entity.CertificateAuthority and updates the ca info + UpdateByUUID(interface{}) error + // GetFirst returns the first *entity.CertificateAuthority + GetFirst() (interface{}, error) +} diff --git a/server/domain/repo/certificate_binding_repo.go b/server/domain/repo/certificate_binding_repo.go new file mode 100644 index 00000000..36d04a19 --- /dev/null +++ b/server/domain/repo/certificate_binding_repo.go @@ -0,0 +1,27 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// CertificateBindingRepository is the interface to handle certificate binding's persistence related actions +type CertificateBindingRepository interface { + // Create takes a *entity.CertificateBinding and creates a record in the repository + Create(interface{}) error + // ListByCertificateUUID returns []entity.CertificateBinding of the specified certificate + ListByCertificateUUID(string) (interface{}, error) + // DeleteByParticipantUUID deletes certificate binding info with the specified participant uuid + DeleteByParticipantUUID(string) error + // ListByParticipantUUID returns []entity.CertificateBinding of the specified participant uuid + ListByParticipantUUID(string) (interface{}, error) +} diff --git a/server/domain/repo/certificate_repo.go b/server/domain/repo/certificate_repo.go new file mode 100644 index 00000000..be9786b6 --- /dev/null +++ b/server/domain/repo/certificate_repo.go @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// CertificateRepository is the interface to handle certificate's persistence related actions +type CertificateRepository interface { + // Create takes a *entity.Certificate and creates a record in the repository + Create(interface{}) error + // List returns []entity.Certificate of all saved certificates + List() (interface{}, error) + // DeleteByUUID deletes certificate info with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.Certificate with the specified UUID + GetByUUID(string) (interface{}, error) + // GetBySerialNumber returns an *entity.Certificate with the specified serial number + GetBySerialNumber(string) (interface{}, error) +} diff --git a/server/domain/repo/chart_repo.go b/server/domain/repo/chart_repo.go new file mode 100644 index 00000000..2734c37a --- /dev/null +++ b/server/domain/repo/chart_repo.go @@ -0,0 +1,29 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ChartRepository is the interface to handle chart's persistence related actions +type ChartRepository interface { + // Create takes a *entity.Chart creates a chart info record in the repository + Create(interface{}) error + // List returns []entity.Chart of all saved chart + List() (interface{}, error) + // DeleteByUUID delete the chart of the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.Chart of the specified uuid + GetByUUID(string) (interface{}, error) + // ListByType takes an entity.ChartType and returns []entity.Chart that is for the specified type + ListByType(interface{}) (interface{}, error) +} diff --git a/server/domain/repo/endpoint_repo.go b/server/domain/repo/endpoint_repo.go new file mode 100644 index 00000000..561cbcb9 --- /dev/null +++ b/server/domain/repo/endpoint_repo.go @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// EndpointRepository is the interface to handle endpoint's persistence related actions +type EndpointRepository interface { + // Create takes a *entity.EndpointBase or its derived struct instance and creates an endpoint info record in the repository + Create(interface{}) error + // List returns []entity.EndpointBase or derived struct instances list + List() (interface{}, error) + // DeleteByUUID delete the endpoint record with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.EndpointBase or its derived struct of the specified uuid + GetByUUID(string) (interface{}, error) + // ListByInfraProviderUUID returns []entity.EndpointBase or derived struct instances list that contain the specified infra uuid + ListByInfraProviderUUID(string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.EndpointBase or its derived struct and updates the status field + UpdateStatusByUUID(interface{}) error + // UpdateInfoByUUID takes an *entity.EndpointBase or its derived struct and updates endpoint editable fields + UpdateInfoByUUID(interface{}) error +} diff --git a/server/domain/repo/event_repo.go b/server/domain/repo/event_repo.go new file mode 100644 index 00000000..51af128b --- /dev/null +++ b/server/domain/repo/event_repo.go @@ -0,0 +1,23 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// EventRepository is the interface to handle event's persistence related actions +type EventRepository interface { + // Create takes a *entity.Event and creates an event record in the repository + Create(interface{}) error + // ListByEntityUUID returns []entity.Event instances list that contain the specified entity uuid + ListByEntityUUID(string) (interface{}, error) +} diff --git a/server/domain/repo/federation_repo.go b/server/domain/repo/federation_repo.go new file mode 100644 index 00000000..cf61c9ec --- /dev/null +++ b/server/domain/repo/federation_repo.go @@ -0,0 +1,27 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// FederationRepository is the interface to handle federation's persistence related actions +type FederationRepository interface { + // Create takes a *entity.Federation's derived struct and creates a federation record in the repository + Create(interface{}) error + // List returns federation list containing entity.Federation's derived struct, such as []entity.FederationFATE + List() (interface{}, error) + // DeleteByUUID delete federation with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.Federation's derived struct of the specified federation + GetByUUID(string) (interface{}, error) +} diff --git a/server/domain/repo/infra_provider_repo.go b/server/domain/repo/infra_provider_repo.go new file mode 100644 index 00000000..7d2343c7 --- /dev/null +++ b/server/domain/repo/infra_provider_repo.go @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// InfraProviderRepository is the interface to handle infra provider's persistence related actions +type InfraProviderRepository interface { + // Create takes a *entity.InfraProviderBase's derived struct and creates a provider info record in the repository + Create(interface{}) error + // List returns provider info list, currently []entity.InfraProviderKubernetes + List() (interface{}, error) + // DeleteByUUID delete provider info with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.InfraProviderBase or its derived struct of the specified provider + GetByUUID(string) (interface{}, error) + // UpdateByUUID takes an *entity.InfraProviderBase or its derived struct and updates the infra provider config by uuid + UpdateByUUID(interface{}) error + // GetByAddress returns an *entity.InfraProviderBase or its derived struct who contains the specified address + GetByAddress(string) (interface{}, error) + // ProviderExists checks if a provider already exists in the database + ProviderExists(interface{}) error +} diff --git a/server/domain/repo/mock/certificate_authority_repo_mock.go b/server/domain/repo/mock/certificate_authority_repo_mock.go new file mode 100644 index 00000000..c94c99a7 --- /dev/null +++ b/server/domain/repo/mock/certificate_authority_repo_mock.go @@ -0,0 +1,39 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +type CertificateAuthorityRepoMock struct { + CreateFn func(instance interface{}) error + UpdateByUUIDFn func(instance interface{}) error + GetFirstFn func() (interface{}, error) +} + +func (m *CertificateAuthorityRepoMock) Create(instance interface{}) error { + return m.CreateFn(instance) +} + +func (m *CertificateAuthorityRepoMock) UpdateByUUID(instance interface{}) error { + return m.UpdateByUUIDFn(instance) +} + +func (m *CertificateAuthorityRepoMock) GetFirst() (interface{}, error) { + return m.GetFirstFn() +} + +var _ repo.CertificateAuthorityRepository = (*CertificateAuthorityRepoMock)(nil) diff --git a/server/domain/repo/mock/endpoint_repo_mock.go b/server/domain/repo/mock/endpoint_repo_mock.go new file mode 100644 index 00000000..1370f941 --- /dev/null +++ b/server/domain/repo/mock/endpoint_repo_mock.go @@ -0,0 +1,57 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import "github.com/FederatedAI/FedLCM/server/domain/repo" + +type EndpointKubeFATERepoMock struct { + CreateFn func(instance interface{}) error + ListFn func() (interface{}, error) + DeleteByUUIDFn func(uuid string) error + GetByUUIDFn func(uuid string) (interface{}, error) + ListByInfraProviderUUIDFn func(uuid string) (interface{}, error) + UpdateStatusByUUIDFn func(instance interface{}) error + UpdateInfoByUUIDFn func(instance interface{}) error +} + +func (m *EndpointKubeFATERepoMock) Create(instance interface{}) error { + return m.CreateFn(instance) +} + +func (m *EndpointKubeFATERepoMock) List() (interface{}, error) { + return m.ListFn() +} + +func (m *EndpointKubeFATERepoMock) DeleteByUUID(uuid string) error { + return m.DeleteByUUIDFn(uuid) +} + +func (m *EndpointKubeFATERepoMock) GetByUUID(uuid string) (interface{}, error) { + return m.GetByUUIDFn(uuid) +} + +func (m *EndpointKubeFATERepoMock) ListByInfraProviderUUID(uuid string) (interface{}, error) { + return m.ListByInfraProviderUUIDFn(uuid) +} + +func (m *EndpointKubeFATERepoMock) UpdateStatusByUUID(instance interface{}) error { + return m.UpdateStatusByUUIDFn(instance) +} + +func (m *EndpointKubeFATERepoMock) UpdateInfoByUUID(instance interface{}) error { + return m.UpdateInfoByUUIDFn(instance) +} + +var _ repo.EndpointRepository = (*EndpointKubeFATERepoMock)(nil) diff --git a/server/domain/repo/mock/federation_repo_mock.go b/server/domain/repo/mock/federation_repo_mock.go new file mode 100644 index 00000000..d779579a --- /dev/null +++ b/server/domain/repo/mock/federation_repo_mock.go @@ -0,0 +1,59 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +type FederationFATERepoMock struct { + CreateFn func(instance interface{}) error + ListFn func() (interface{}, error) + DeleteByUUIDfn func(uuid string) error + GetByUUIDfn func(uuid string) (interface{}, error) +} + +func (m *FederationFATERepoMock) Create(instance interface{}) error { + if m.CreateFn != nil { + return m.CreateFn(instance) + } + return nil +} + +func (m *FederationFATERepoMock) List() (interface{}, error) { + if m.ListFn != nil { + return m.ListFn() + } + return nil, nil +} + +func (m *FederationFATERepoMock) DeleteByUUID(uuid string) error { + if m.DeleteByUUIDfn != nil { + return m.DeleteByUUIDfn(uuid) + } + return nil +} + +func (m *FederationFATERepoMock) GetByUUID(uuid string) (interface{}, error) { + if m.GetByUUIDfn != nil { + return m.GetByUUIDfn(uuid) + } + return &entity.FederationFATE{ + Domain: "test.example.com", + }, nil +} + +var _ repo.FederationRepository = (*FederationFATERepoMock)(nil) diff --git a/server/domain/repo/mock/infra_provider_repo_mock.go b/server/domain/repo/mock/infra_provider_repo_mock.go new file mode 100644 index 00000000..cbe1e1b2 --- /dev/null +++ b/server/domain/repo/mock/infra_provider_repo_mock.go @@ -0,0 +1,56 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import "github.com/FederatedAI/FedLCM/server/domain/repo" + +type InfraProviderKubernetesRepoMock struct { + CreateFn func(instance interface{}) error + ListFn func() (interface{}, error) + DeleteByUUIDFn func(uuid string) error + GetByUUIDFn func(uuid string) (interface{}, error) + UpdateByUUIDFn func(instance interface{}) error + GetByAddressFn func(address string) (interface{}, error) +} + +func (m *InfraProviderKubernetesRepoMock) ProviderExists(instance interface{}) error { + return m.ProviderExists(instance) +} + +func (m *InfraProviderKubernetesRepoMock) Create(instance interface{}) error { + return m.CreateFn(instance) +} + +func (m *InfraProviderKubernetesRepoMock) List() (interface{}, error) { + return m.ListFn() +} + +func (m *InfraProviderKubernetesRepoMock) DeleteByUUID(uuid string) error { + return m.DeleteByUUIDFn(uuid) +} + +func (m *InfraProviderKubernetesRepoMock) GetByUUID(uuid string) (interface{}, error) { + return m.GetByUUIDFn(uuid) +} + +func (m *InfraProviderKubernetesRepoMock) UpdateByUUID(instance interface{}) error { + return m.UpdateByUUIDFn(instance) +} + +func (m *InfraProviderKubernetesRepoMock) GetByAddress(address string) (interface{}, error) { + return m.GetByAddressFn(address) +} + +var _ repo.InfraProviderRepository = (*InfraProviderKubernetesRepoMock)(nil) diff --git a/server/domain/repo/mock/participant_fate_repo_mock.go b/server/domain/repo/mock/participant_fate_repo_mock.go new file mode 100644 index 00000000..2de2dfb1 --- /dev/null +++ b/server/domain/repo/mock/participant_fate_repo_mock.go @@ -0,0 +1,121 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +type ParticipantFATERepoMock struct { + CreateFn func(instance interface{}) error + ListFn func() (interface{}, error) + DeleteByUUIDFn func(uuid string) error + GetByUUIDFn func(uuid string) (interface{}, error) + ListByFederationUUIDFn func(uuid string) (interface{}, error) + ListByEndpointUUIDFn func(uuid string) (interface{}, error) + UpdateStatusByUUIDFn func(instance interface{}) error + UpdateDeploymentYAMLByUUIDFn func(instance interface{}) error + UpdateInfoByUUIDFn func(instance interface{}) error + IsExchangeCreatedByFederationUUIDFn func(uuid string) (bool, error) + GetExchangeByFederationUUIDFn func(uuid string) (interface{}, error) + IsConflictedByFederationUUIDAndPartyIDFn func(uuid string, partyID int) (bool, error) +} + +func (m *ParticipantFATERepoMock) Create(instance interface{}) error { + if m.CreateFn != nil { + return m.CreateFn(instance) + } + return nil +} + +func (m *ParticipantFATERepoMock) List() (interface{}, error) { + if m.ListFn != nil { + return m.ListFn() + } + return nil, nil +} + +func (m *ParticipantFATERepoMock) DeleteByUUID(uuid string) error { + if m.DeleteByUUIDFn != nil { + return m.DeleteByUUIDFn(uuid) + } + return nil +} + +func (m *ParticipantFATERepoMock) GetByUUID(uuid string) (interface{}, error) { + if m.GetByUUIDFn != nil { + return m.GetByUUIDFn(uuid) + } + return &entity.ParticipantFATE{}, nil +} + +func (m *ParticipantFATERepoMock) ListByFederationUUID(uuid string) (interface{}, error) { + if m.ListByFederationUUIDFn != nil { + return m.ListByFederationUUIDFn(uuid) + } + return nil, nil +} + +func (m *ParticipantFATERepoMock) ListByEndpointUUID(uuid string) (interface{}, error) { + if m.ListByEndpointUUIDFn != nil { + return m.ListByEndpointUUIDFn(uuid) + } + return nil, nil +} + +func (m *ParticipantFATERepoMock) UpdateStatusByUUID(instance interface{}) error { + if m.UpdateStatusByUUIDFn != nil { + return m.UpdateStatusByUUIDFn(instance) + } + return nil +} + +func (m *ParticipantFATERepoMock) UpdateDeploymentYAMLByUUID(instance interface{}) error { + if m.UpdateDeploymentYAMLByUUIDFn != nil { + return m.UpdateDeploymentYAMLByUUIDFn(instance) + } + return nil +} + +func (m *ParticipantFATERepoMock) UpdateInfoByUUID(instance interface{}) error { + if m.UpdateInfoByUUIDFn != nil { + return m.UpdateInfoByUUIDFn(instance) + } + return nil +} + +func (m *ParticipantFATERepoMock) IsExchangeCreatedByFederationUUID(uuid string) (bool, error) { + if m.IsExchangeCreatedByFederationUUIDFn != nil { + return m.IsExchangeCreatedByFederationUUIDFn(uuid) + } + return false, nil +} + +func (m *ParticipantFATERepoMock) GetExchangeByFederationUUID(uuid string) (interface{}, error) { + if m.GetExchangeByFederationUUIDFn != nil { + return m.GetExchangeByFederationUUIDFn(uuid) + } + return nil, nil +} + +func (m *ParticipantFATERepoMock) IsConflictedByFederationUUIDAndPartyID(uuid string, partyID int) (bool, error) { + if m.IsConflictedByFederationUUIDAndPartyIDFn != nil { + return m.IsConflictedByFederationUUIDAndPartyIDFn(uuid, partyID) + } + return false, nil +} + +var _ repo.ParticipantFATERepository = (*ParticipantFATERepoMock)(nil) diff --git a/server/domain/repo/participant_fate_repo.go b/server/domain/repo/participant_fate_repo.go new file mode 100644 index 00000000..5f4c6555 --- /dev/null +++ b/server/domain/repo/participant_fate_repo.go @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ParticipantFATERepository contains FATE participant specific operations in addition to ParticipantRepository +type ParticipantFATERepository interface { + ParticipantRepository + // IsExchangeCreatedByFederationUUID returns whether an exchange exists in the specified federation + IsExchangeCreatedByFederationUUID(string) (bool, error) + // GetExchangeByFederationUUID returns an *entity.ParticipantFATE that is the exchange of the specified federation + GetExchangeByFederationUUID(string) (interface{}, error) + // IsConflictedByFederationUUIDAndPartyID returns whether a party id in a federation is already used + IsConflictedByFederationUUIDAndPartyID(string, int) (bool, error) +} diff --git a/server/domain/repo/participant_openfl_repo.go b/server/domain/repo/participant_openfl_repo.go new file mode 100644 index 00000000..cc661918 --- /dev/null +++ b/server/domain/repo/participant_openfl_repo.go @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ParticipantOpenFLRepository contains OpenFL specific operations in addition to ParticipantRepository +type ParticipantOpenFLRepository interface { + ParticipantRepository + // IsDirectorCreatedByFederationUUID returns whether a director exists in the specified federation + IsDirectorCreatedByFederationUUID(string) (bool, error) + // CountByTokenUUID returns the number of participant using a specified token + CountByTokenUUID(string) (int, error) + // GetDirectorByFederationUUID returns an *entity.ParticipantOpenFL that is the director of the specified federation + GetDirectorByFederationUUID(string) (interface{}, error) +} diff --git a/server/domain/repo/participant_repo.go b/server/domain/repo/participant_repo.go new file mode 100644 index 00000000..b6a27bdc --- /dev/null +++ b/server/domain/repo/participant_repo.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ParticipantRepository contains common repo interface to work with federation participants +type ParticipantRepository interface { + // Create takes an *entity.Participant's derived struct and creates a fate participant info record in the repo + Create(interface{}) error + // List returns []entity.Participant's derived struct list + List() (interface{}, error) + // DeleteByUUID deletes participant info with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.Participant's derived struct of the specified uuid + GetByUUID(string) (interface{}, error) + // ListByFederationUUID returns []entity.Participant's derived struct list that contain the specified federation uuid + ListByFederationUUID(string) (interface{}, error) + // ListByEndpointUUID returns []entity.Participant's derived struct list that contain the specified endpoint uuid + ListByEndpointUUID(string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.Participant's derived struct and updates the status field + UpdateStatusByUUID(interface{}) error + // UpdateDeploymentYAMLByUUID takes an *entity.Participant's derived struct and updates the deployment_yaml field + UpdateDeploymentYAMLByUUID(interface{}) error + // UpdateInfoByUUID takes a *entity.Participant and updates the participant editable fields + UpdateInfoByUUID(interface{}) error +} diff --git a/server/domain/repo/registration_token_repo.go b/server/domain/repo/registration_token_repo.go new file mode 100644 index 00000000..4c30ca0d --- /dev/null +++ b/server/domain/repo/registration_token_repo.go @@ -0,0 +1,31 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// RegistrationTokenRepository is the interface to handle federation's registration token related actions +type RegistrationTokenRepository interface { + // Create takes an *entity.RegistrationToken's derived struct and creates a record in the repository + Create(interface{}) error + // ListByFederation returns token list, currently []entity.RegistrationTokenOpenFL + ListByFederation(string) (interface{}, error) + // DeleteByUUID delete the token with the specified uuid + DeleteByUUID(string) error + // GetByUUID returns an *entity.RegistrationToken's derived struct of the specified uuid + GetByUUID(string) (interface{}, error) + // LoadByTypeAndStr takes an *entity.RegistrationToken's derived struct and fill it with missing info based on the type and token string + LoadByTypeAndStr(interface{}) error + // DeleteByFederation deletes all tokens within the specified federation + DeleteByFederation(string) error +} diff --git a/server/domain/repo/user_repo.go b/server/domain/repo/user_repo.go new file mode 100644 index 00000000..0dd82d15 --- /dev/null +++ b/server/domain/repo/user_repo.go @@ -0,0 +1,27 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// UserRepository holds methods to access user repos +type UserRepository interface { + // CreateUser takes an *entity.User and creates a new user record in the repo + CreateUser(user interface{}) error + // LoadById takes an *entity.User and loads info of a user with the specified id from the repo into it + LoadById(user interface{}) error + // LoadByName takes an *entity.User loads info of a user with the specified name from the repo into it + LoadByName(user interface{}) error + // UpdatePasswordById updates a users password + UpdatePasswordById(id uint, newPassword string) error +} diff --git a/server/domain/service/certificate_service.go b/server/domain/service/certificate_service.go new file mode 100644 index 00000000..6bf57cf3 --- /dev/null +++ b/server/domain/service/certificate_service.go @@ -0,0 +1,144 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "github.com/smallstep/certificates/cas/apiv1" +) + +// CertificateService provides functions to work with certificate related workflows +type CertificateService struct { + CertificateAuthorityRepo repo.CertificateAuthorityRepository + CertificateRepo repo.CertificateRepository + CertificateBindingRepo repo.CertificateBindingRepository +} + +// CreateCertificateSimple just take the CN, lifetime and dnsNames and will give the automatically generated private key and the certificate using the first available CA +func (s *CertificateService) CreateCertificateSimple(commonName string, lifetime time.Duration, dnsNames []string) (cert *entity.Certificate, pk *rsa.PrivateKey, err error) { + + certificateAuthority, err := s.DefaultCA() + if err != nil { + return + } + caClient, err := certificateAuthority.Client() + if err != nil { + return + } + pk, err = rsa.GenerateKey(rand.Reader, 2048) + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: commonName, + }, + DNSNames: dnsNames, + }, pk) + if err != nil { + return + } + + csr, err := x509.ParseCertificateRequest(csrBytes) + if err != nil { + return + } + + resp, err := caClient.CreateCertificate(&apiv1.CreateCertificateRequest{ + CSR: csr, + Lifetime: lifetime, + }) + if err != nil { + return + } + cert = &entity.Certificate{ + UUID: uuid.NewV4().String(), + Name: fmt.Sprintf("Certificate for %s", commonName), + Certificate: resp.Certificate, + Chain: resp.CertificateChain, + } + + if saveErr := s.CertificateRepo.Create(cert); saveErr != nil { + err = errors.Wrap(err, "failed to save certificate") + } + return +} + +// DefaultCA returns the default CA info +func (s *CertificateService) DefaultCA() (*entity.CertificateAuthority, error) { + instance, err := s.CertificateAuthorityRepo.GetFirst() + if err != nil { + return nil, err + } + certificateAuthority := instance.(*entity.CertificateAuthority) + return certificateAuthority, nil +} + +// CreateBinding create a binding record of a certificate and a participant +func (s *CertificateService) CreateBinding(cert *entity.Certificate, + serviceType entity.CertificateBindingServiceType, + participantUUID string, + federationUUID string, + federationType entity.FederationType) error { + return s.CertificateBindingRepo.Create(&entity.CertificateBinding{ + UUID: uuid.NewV4().String(), + CertificateUUID: cert.UUID, + ParticipantUUID: participantUUID, + ServiceType: serviceType, + FederationUUID: federationUUID, + FederationType: federationType, + }) +} + +// RemoveBinding deletes a bindings record and deletes the certificate if there is no bindings for it +func (s *CertificateService) RemoveBinding(participantUUID string) error { + instanceList, err := s.CertificateBindingRepo.ListByParticipantUUID(participantUUID) + if err != nil { + return err + } + bindingList := instanceList.([]entity.CertificateBinding) + certificateUUIDSet := map[string]interface{}{} + for _, binding := range bindingList { + certificateUUIDSet[binding.CertificateUUID] = nil + } + if err := s.CertificateBindingRepo.DeleteByParticipantUUID(participantUUID); err != nil { + return errors.Wrapf(err, "failed to delete bindings") + } + for certificateUUID := range certificateUUIDSet { + instanceList, err = s.CertificateBindingRepo.ListByCertificateUUID(certificateUUID) + if err != nil { + return errors.Wrapf(err, "failed to query bindings") + } + if instanceList != nil { + bindingList = instanceList.([]entity.CertificateBinding) + if len(bindingList) != 0 { + continue + } + } + log.Info().Str("certificate uuid", certificateUUID).Msgf("removing unused certificate") + if err := s.CertificateRepo.DeleteByUUID(certificateUUID); err != nil { + log.Err(err).Msgf("failed to remove certificate %s", certificateUUID) + } + } + return nil +} diff --git a/server/domain/service/endpoint_service.go b/server/domain/service/endpoint_service.go new file mode 100644 index 00000000..e12e19c3 --- /dev/null +++ b/server/domain/service/endpoint_service.go @@ -0,0 +1,583 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "bytes" + "fmt" + "net/url" + "strings" + "text/template" + "time" + + "github.com/FederatedAI/FedLCM/pkg/kubefate" + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/pkg/utils" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/hashicorp/go-version" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + k8sErr "k8s.io/apimachinery/pkg/api/errors" +) + +// EndpointService is the domain service for the endpoint management +type EndpointService struct { + InfraProviderKubernetesRepo repo.InfraProviderRepository + EndpointKubeFATERepo repo.EndpointRepository + ParticipantFATERepo repo.ParticipantFATERepository + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + EventService EventServiceInt +} + +// EndpointScanResult records the scanning result from an infra provider +type EndpointScanResult struct { + *entity.EndpointBase + IsManaged bool + IsCompatible bool +} + +var minKubeFATEVersion = func() *version.Version { + version, _ := version.NewVersion("1.4.4") + return version +}() + +var inCompatibleEndpoint = EndpointScanResult{ + EndpointBase: &entity.EndpointBase{ + UUID: "", + Name: "incompatible endpoint", + Description: "", + Type: entity.EndpointTypeKubeFATE, + }, + IsManaged: false, + IsCompatible: false, +} + +var compatibleEndpoint = EndpointScanResult{ + EndpointBase: &entity.EndpointBase{ + UUID: "", + Name: "compatible endpoint", + Description: "", + Type: entity.EndpointTypeKubeFATE, + }, + IsManaged: false, + IsCompatible: true, +} + +// for mocking purpose +var ( + newK8sClientFn = kubernetes.NewKubernetesClient + newKubeFATEMgrFn = kubefate.NewManager +) + +const ( + imagePullSecretsNameKubeFATE = "registrykeykubefate" +) + +// CreateKubeFATEEndpoint install a kubefate endpoint or add an existing kubefate as a managed endpoint +func (s *EndpointService) CreateKubeFATEEndpoint(infraUUID, name, description, yaml string, + install bool, + ingressControllerServiceMode entity.EndpointKubeFATEIngressControllerServiceMode) (string, error) { + log.Info().Msgf("creating KubeFATE endpoint with name: %s, install: %v on infra with uuid: %s", name, install, infraUUID) + if name == "" { + return "", errors.New("name cannot be empty") + } + endpointListInstance, err := s.EndpointKubeFATERepo.ListByInfraProviderUUID(infraUUID) + if err != nil { + return "", errors.Wrapf(err, "failed to query current infra's KubeFATE endpoint info") + } + domainEndpointList := endpointListInstance.([]entity.EndpointKubeFATE) + if len(domainEndpointList) != 0 { + return "", errors.Errorf("current infra provider %s already contains a KubeFATE endpoint", infraUUID) + } + + endpoint := &entity.EndpointKubeFATE{ + EndpointBase: entity.EndpointBase{ + UUID: uuid.NewV4().String(), + InfraProviderUUID: infraUUID, + Name: name, + Description: description, + Version: "", + Type: entity.EndpointTypeKubeFATE, + Status: entity.EndpointStatusCreating, + }, + Config: entity.KubeFATEConfig{ + IngressAddress: "", + IngressRuleHost: "", + UsePortForwarding: ingressControllerServiceMode == entity.EndpointKubeFATEIngressControllerServiceModeModeNonexistent, + }, + DeploymentYAML: "", + IngressControllerYAML: "", + } + + ingressControllerYAML := "" + if install { + if !endpoint.Config.UsePortForwarding { + log.Info().Msgf("ingress controller is needed, service mode: %v", ingressControllerServiceMode) + ingressControllerYAML, err = s.GetIngressControllerDeploymentYAML(ingressControllerServiceMode) + if err != nil { + return "", err + } + } + if yaml == "" { + log.Info().Msgf("generating default deployment yaml") + yaml, err = s.GetDeploymentYAML("admin", "admin", "kubefate.net", valueobject.KubeRegistryConfig{}) + if err != nil { + return "", err + } + } + } + mgr, err := s.BuildKubeFATEManager(infraUUID, yaml, ingressControllerYAML) + if err != nil { + return "", errors.Wrapf(err, "failed to build kubefate manager") + } + + fillInfo := func() error { + kfc, closer, err := mgr.BuildPFClient() + if closer != nil { + defer closer() + } + if err != nil { + return errors.Wrapf(err, "failed to get kubefate client") + } + versionStr, err := kfc.CheckVersion() + if err != nil { + return errors.Wrapf(err, "failed to get kubefate version") + } + endpoint.Version = versionStr + endpoint.Config.IngressAddress = kfc.IngressAddress() + endpoint.Config.IngressRuleHost = kfc.IngressRuleHost() + endpoint.Description = strings.TrimSpace(endpoint.Description + " The address is a dummy one because we will use port-forwarder to do the connection.") + return nil + } + + if install { + endpoint.DeploymentYAML = yaml + endpoint.IngressControllerYAML = ingressControllerYAML + } else { + // ignore ingress requirements when adding existing KubeFATE + log.Info().Msgf("adding existing KubeFATE instead of installing a new one") + endpoint.Config.UsePortForwarding = true + if err := fillInfo(); err != nil { + return "", errors.Wrapf(err, "failed to query existing kubefate info") + } + endpoint.Status = entity.EndpointStatusReady + } + if err := s.EndpointKubeFATERepo.Create(endpoint); err != nil { + return "", errors.Wrapf(err, "failed to create endpoint") + } + //record event of creating endpoint + err = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, "start creating endpoint", entity.EventLogLevelInfo) + if err != nil { + return endpoint.UUID, err + } + if install { + go func() { + message := fmt.Sprintf("installing kubefate endpoint (%s) on infra provider: %s", name, infraUUID) + log.Info().Msgf(message) + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, message, entity.EventLogLevelInfo) + err := func() error { + if endpoint.IngressControllerYAML != "" { + message = fmt.Sprintf("installing ingress controller") + log.Info().Msgf(message) + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, message, entity.EventLogLevelInfo) + if err := mgr.InstallIngressNginxController(); err != nil { + return errors.Wrapf(err, "failed to install ingress controller") + } + } + message = fmt.Sprintf("remove any old KubeFATE instance") + log.Info().Msgf(message) + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, message, entity.EventLogLevelInfo) + if err := mgr.Uninstall(); err != nil { + return errors.Wrapf(err, "failed to uninstall old installation") + } + message = fmt.Sprintf("installing new KubeFATE instance") + log.Info().Msgf(message) + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, message, entity.EventLogLevelInfo) + if err := mgr.Install(!endpoint.Config.UsePortForwarding); err != nil { + return errors.Wrapf(err, "failed to install kubefate") + } + if err := fillInfo(); err != nil { + return errors.Wrapf(err, "failed to get installed kubefate info") + } + endpoint.Status = entity.EndpointStatusReady + if err := s.EndpointKubeFATERepo.UpdateInfoByUUID(endpoint); err != nil { + return errors.Wrapf(err, "failed to update endpoint info") + } + message = fmt.Sprintf("KubeFATE installed without error") + log.Info().Msgf(message) + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, message, entity.EventLogLevelInfo) + return nil + }() + if err != nil { + message = "error installing kubefate endpoint" + log.Err(err).Msgf(message) + //record event of error in installing + eventDesc := errors.Wrapf(err, message).Error() + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, eventDesc, entity.EventLogLevelError) + endpoint.Status = entity.EndpointStatusUnavailable + if err := s.EndpointKubeFATERepo.UpdateStatusByUUID(endpoint); err != nil { + message = "failed to update endpoint status" + log.Err(err).Msg(message) + //record event of error in updating endpoint status + eventDesc := errors.Wrapf(err, message).Error() + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, endpoint.UUID, eventDesc, entity.EventLogLevelError) + } + } + }() + } + return endpoint.UUID, nil +} + +// FindKubeFATEEndpoint returns endpoints installation status from an infra provider +func (s *EndpointService) FindKubeFATEEndpoint(infraUUID string) ([]EndpointScanResult, error) { + // 1. find from repo + endpointListInstance, err := s.EndpointKubeFATERepo.ListByInfraProviderUUID(infraUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get KubeFATE endpoint list") + } + domainEndpointList := endpointListInstance.([]entity.EndpointKubeFATE) + if len(domainEndpointList) != 0 { + var resultList []EndpointScanResult + for _, domainEndpoint := range domainEndpointList { + resultList = append(resultList, EndpointScanResult{ + EndpointBase: &entity.EndpointBase{ + Model: domainEndpoint.Model, + UUID: domainEndpoint.UUID, + InfraProviderUUID: infraUUID, + Name: domainEndpoint.Name, + Description: domainEndpoint.Description, + Type: domainEndpoint.Type, + Status: domainEndpoint.Status, + }, + IsManaged: true, + IsCompatible: true, + }) + } + return resultList, nil + } + + // 2. scan the infra + kubefateMgr, err := s.BuildKubeFATEManager(infraUUID, "", "") + if err != nil { + return nil, errors.Wrapf(err, "failed to get kubefate manager") + } + deployment, err := kubefateMgr.GetKubeFATEDeployment() + if err != nil { + if k8sErr.IsNotFound(err) { + log.Info().Msgf("no kubefate endpoint found in infra: %s", infraUUID) + return nil, nil + } + return nil, errors.Wrapf(err, "error querying kubefate installation") + } + + // we don't require ingress to be available so we can use the client based on port-forward + kfClient, closer, err := kubefateMgr.BuildPFClient() + if closer != nil { + defer closer() + } + if err != nil { + return []EndpointScanResult{ + inCompatibleEndpoint, + }, nil + } + + versionStr, err := kfClient.CheckVersion() + if err != nil { + return []EndpointScanResult{ + inCompatibleEndpoint, + }, nil + } + + currentVersion, err := version.NewVersion(versionStr) + if err != nil { + return []EndpointScanResult{ + inCompatibleEndpoint, + }, nil + } + if currentVersion.LessThan(minKubeFATEVersion) { + return []EndpointScanResult{ + inCompatibleEndpoint, + }, nil + } + + compatibleEndpointCopy := compatibleEndpoint + compatibleEndpointCopy.CreatedAt = deployment.CreationTimestamp.Time + return []EndpointScanResult{ + compatibleEndpointCopy, + }, nil +} + +// TestKubeFATE checks a KubeFATE connection +func (s *EndpointService) TestKubeFATE(uuid string) error { + endpointInstance, err := s.EndpointKubeFATERepo.GetByUUID(uuid) + if err != nil { + return errors.Wrapf(err, "failed to get KubeFAET endpoint instance") + } + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + + if err := func() error { + kubefateMgr, err := s.buildKubeFATEClientManagerFromEndpointUUID(uuid) + if err != nil { + return err + } + // for testing purpose we use PFClient + kfClient, closer, err := kubefateMgr.BuildPFClient() + if closer != nil { + defer closer() + } + if err != nil { + return errors.Wrapf(err, "failed to build kubefate client") + } + + _, err = kfClient.CheckVersion() + if err != nil { + return errors.Wrapf(err, "failed to get kubefate str") + } + endpoint.Status = entity.EndpointStatusReady + if endpoint.Config.UsePortForwarding { + endpoint.Config.IngressRuleHost = kfClient.IngressRuleHost() + endpoint.Config.IngressAddress = kfClient.IngressAddress() + } + return nil + }(); err != nil { + endpoint.Status = entity.EndpointStatusUnavailable + return err + } + + _ = s.EndpointKubeFATERepo.UpdateInfoByUUID(endpoint) + return nil +} + +// RemoveEndpoint removes the specified endpoint +func (s *EndpointService) RemoveEndpoint(uuid string, uninstall bool) error { + log.Info().Msgf("remove endpoint with uuid %s, uninstall: %v", uuid, uninstall) + endpointInstance, err := s.EndpointKubeFATERepo.GetByUUID(uuid) + if err != nil { + return errors.Wrapf(err, "failed to get KubeFAET endpoint instance") + } + domainEndpointKubeFATE := endpointInstance.(*entity.EndpointKubeFATE) + + instanceList, err := s.ParticipantFATERepo.ListByEndpointUUID(uuid) + if err != nil { + return errors.Wrap(err, "failed to query endpoint participants") + } + participantList := instanceList.([]entity.ParticipantFATE) + if len(participantList) > 0 { + return errors.Errorf("cannot remove endpoint that still contains %v participants", len(participantList)) + } + + instanceListOpenFL, err := s.ParticipantOpenFLRepo.ListByEndpointUUID(uuid) + if err != nil { + return errors.Wrap(err, "failed to query endpoint participants") + } + + participantListOpenFL := instanceListOpenFL.([]entity.ParticipantOpenFL) + if len(participantListOpenFL) > 0 { + return errors.Errorf("cannot remove endpoint that still contains %v OpenFL participants", len(participantListOpenFL)) + } + + domainEndpointKubeFATE.Status = entity.EndpointStatusDeleting + if err := s.EndpointKubeFATERepo.UpdateStatusByUUID(domainEndpointKubeFATE); err != nil { + return errors.Wrapf(err, "failed to update status to deleting") + } + + err = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, uuid, "start deleting endpoint", entity.EventLogLevelInfo) + if err != nil { + return err + } + go func() { + // continue the removing even if uninstallation failed + if uninstall { + kubefateMgr, err := s.BuildKubeFATEManager(domainEndpointKubeFATE.InfraProviderUUID, domainEndpointKubeFATE.DeploymentYAML, domainEndpointKubeFATE.IngressControllerYAML) + if err != nil { + message := "failed to get kubefate manager" + log.Err(err).Msgf(message) + //record error event + eventDesc := errors.Wrapf(err, message).Error() + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, uuid, eventDesc, entity.EventLogLevelError) + } else if err := kubefateMgr.Uninstall(); err != nil { + message := "failed to remove kubefate installation" + log.Err(err).Msgf(message) + //record error event + eventDesc := errors.Wrapf(err, message).Error() + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, uuid, eventDesc, entity.EventLogLevelError) + } + } + if err := s.EndpointKubeFATERepo.DeleteByUUID(uuid); err != nil { + message := "failed to delete endpoint" + log.Err(err).Msgf(message) + //record error event + eventDesc := errors.Wrapf(err, message).Error() + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeEndpoint, uuid, eventDesc, entity.EventLogLevelError) + } + }() + return nil +} + +func (s *EndpointService) buildKubeFATEClientManagerFromEndpointUUID(uuid string) (kubefate.ClientManager, error) { + endpointInstance, err := s.EndpointKubeFATERepo.GetByUUID(uuid) + if err != nil { + return nil, errors.Wrapf(err, "failed to get KubeFAET endpoint instance") + } + endpoint := endpointInstance.(*entity.EndpointKubeFATE) + return s.BuildKubeFATEManager(endpoint.InfraProviderUUID, endpoint.DeploymentYAML, endpoint.IngressControllerYAML) +} + +// BuildKubeFATEManager retrieve a KubeFATE manager instance from the provided endpoint uuid +func (s *EndpointService) BuildKubeFATEManager(infraUUID, yaml, ingressControllerYAML string) (kubefate.Manager, error) { + providerInstance, err := s.InfraProviderKubernetesRepo.GetByUUID(infraUUID) + if err != nil { + return nil, errors.Wrapf(err, "error query infra provider") + } + provider := providerInstance.(*entity.InfraProviderKubernetes) + + client, err := newK8sClientFn("", provider.Config.KubeConfigContent) + if err != nil { + return nil, errors.Wrapf(err, "failed to get kubernetes client") + } + + installMeta, err := kubefate.BuildInstallationMetaFromYAML(yaml, ingressControllerYAML) + if err != nil { + return nil, errors.Wrapf(err, "failed to get kubefate meta") + } + kubefateMgr := newKubeFATEMgrFn(client, installMeta) + return kubefateMgr, nil +} + +// GetDeploymentYAML returns the default kubefate deployment yaml +func (s *EndpointService) GetDeploymentYAML(serviceUserName, servicePassword, hostname string, registryConfig valueobject.KubeRegistryConfig) (string, error) { + t, err := template.New("kubefate").Parse(kubefate.GetDefaultYAML()) + if err != nil { + return "", err + } + + registrySecretData := "" + if registryConfig.UseRegistrySecret { + registrySecretData, err = registryConfig.RegistrySecretConfig.BuildSecretB64String() + if err != nil { + return "", err + } + } + data := struct { + ServiceUserName string + ServicePassword string + Hostname string + UseRegistry bool + Registry string + UseImagePullSecrets bool + ImagePullSecretsName string + RegistrySecretData string + }{ + ServiceUserName: serviceUserName, + ServicePassword: servicePassword, + Hostname: hostname, + UseRegistry: registryConfig.UseRegistry, + Registry: registryConfig.Registry, + UseImagePullSecrets: registryConfig.UseRegistrySecret, + ImagePullSecretsName: imagePullSecretsNameKubeFATE, + RegistrySecretData: registrySecretData, + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// GetIngressControllerDeploymentYAML returns the default ingress controller deployment yaml +func (s *EndpointService) GetIngressControllerDeploymentYAML(mode entity.EndpointKubeFATEIngressControllerServiceMode) (string, error) { + if mode == entity.EndpointKubeFATEIngressControllerServiceModeSkip { + return "", nil + } + t, err := template.New("ingress-nginx").Parse(kubefate.GetDefaultIngressControllerYAML()) + if err != nil { + return "", err + } + serviceType := "NodePort" + if mode == entity.EndpointKubeFATEIngressControllerServiceModeLoadBalancer { + serviceType = "LoadBalancer" + } + data := struct { + ServiceType string + }{ + ServiceType: serviceType, + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// ensureEndpointExist returns try to add/install a KubeFATE into the specified cluster +func (s *EndpointService) ensureEndpointExist(infraUUID string) (string, error) { + endpointScanResult, err := s.FindKubeFATEEndpoint(infraUUID) + if err != nil { + return "", err + } + install := true + if len(endpointScanResult) > 0 { + scannedEndpoint := endpointScanResult[0] + if scannedEndpoint.IsManaged { + log.Info().Msgf("re-use endpoint %s(%s)", scannedEndpoint.Name, scannedEndpoint.UUID) + return scannedEndpoint.UUID, nil + } + if scannedEndpoint.IsCompatible { + install = false + } else { + return "", errors.New("infra contains in-compatible KubeFATE") + } + } + providerInstance, err := s.InfraProviderKubernetesRepo.GetByUUID(infraUUID) + if err != nil { + return "", errors.Wrapf(err, "error query infra provider") + } + provider := providerInstance.(*entity.InfraProviderKubernetes) + u, err := url.Parse(provider.APIHost) + if err != nil { + return "", err + } + endpointUUID, err := s.CreateKubeFATEEndpoint(infraUUID, + fmt.Sprintf("kubefate-%s", u.Hostname()), + fmt.Sprintf("Automatically added KubeFATE on Kubernetes %s.", u.Hostname()), + "", + install, + entity.EndpointKubeFATEIngressControllerServiceModeModeNonexistent) + + var installErr error + if err := utils.ExecuteWithTimeout(func() bool { + log.Info().Msgf("checking kubefate endpoint status...") + instance, err := s.EndpointKubeFATERepo.GetByUUID(endpointUUID) + if err != nil { + log.Err(err).Msgf("failed to query endpoint") + return false + } + endpoint := instance.(*entity.EndpointKubeFATE) + if endpoint.Status == entity.EndpointStatusCreating { + log.Info().Msgf("endpoint status is %v, continue querying", endpoint.Status) + return false + } + if endpoint.Status != entity.EndpointStatusReady { + installErr = errors.Errorf("invalid endpoint status: %v, abort", endpoint.Status) + } + return true + }, time.Minute*30, time.Second*10); err != nil { + return "", errors.Wrapf(err, "error checking kubefate ingress deployment") + } + return endpointUUID, installErr +} diff --git a/server/domain/service/event_service.go b/server/domain/service/event_service.go new file mode 100644 index 00000000..c2b42b2b --- /dev/null +++ b/server/domain/service/event_service.go @@ -0,0 +1,54 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + uuid "github.com/satori/go.uuid" +) + +// EventServiceInt provides functions to work with core entities' lifecycle events +type EventServiceInt interface { + // CreateEvent creates a new event record + CreateEvent(eventType entity.EventType, entityType entity.EntityType, entityUUID string, description string, level entity.EventLogLevel) error +} + +// EventService provides functions to work with core entities' lifecycle events +type EventService struct { + EventRepo repo.EventRepository +} + +func (s *EventService) CreateEvent(eventType entity.EventType, entityType entity.EntityType, entityUUID string, description string, level entity.EventLogLevel) error { + data := &entity.EventData{ + Description: description, + LogLevel: level.String(), + } + + eventData, err := json.Marshal(data) + if err != nil { + return err + } + event := &entity.Event{ + UUID: uuid.NewV4().String(), + Type: eventType, + EntityUUID: entityUUID, + EntityType: entityType, + Data: string(eventData), + } + return s.EventRepo.Create(event) +} diff --git a/server/domain/service/mock_event_service_test.go b/server/domain/service/mock_event_service_test.go new file mode 100644 index 00000000..8e0928a4 --- /dev/null +++ b/server/domain/service/mock_event_service_test.go @@ -0,0 +1,28 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" +) + +type mockEventServiceInt struct { +} + +func (m *mockEventServiceInt) CreateEvent(entity.EventType, entity.EntityType, string, string, entity.EventLogLevel) error { + return nil +} + +var _ EventServiceInt = (*mockEventServiceInt)(nil) diff --git a/server/domain/service/mock_kubefate_test.go b/server/domain/service/mock_kubefate_test.go new file mode 100644 index 00000000..59cc8e65 --- /dev/null +++ b/server/domain/service/mock_kubefate_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/pkg/kubefate" + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/modules" + appv1 "k8s.io/api/apps/v1" +) + +type mockKubeFATEClient struct { +} + +func (m *mockKubeFATEClient) CheckVersion() (string, error) { + return "1.4.2", nil +} + +func (m *mockKubeFATEClient) EnsureChartExist(string, string, []byte) error { + return nil +} + +func (m *mockKubeFATEClient) ListClusterByNamespace(string) ([]*modules.Cluster, error) { + return nil, nil +} + +func (m *mockKubeFATEClient) SubmitClusterInstallationJob(string) (string, error) { + return "test-job-id", nil +} + +func (m *mockKubeFATEClient) SubmitClusterUpdateJob(string) (string, error) { + return "test-job-id", nil +} + +func (m *mockKubeFATEClient) SubmitClusterDeletionJob(string) (string, error) { + return "test-job-id", nil +} + +func (m *mockKubeFATEClient) GetJobInfo(string) (*modules.Job, error) { + return &modules.Job{ + ClusterId: "test-cluster-id", + Status: modules.JobStatusSuccess, + }, nil +} + +func (m *mockKubeFATEClient) WaitJob(string) (*modules.Job, error) { + return &modules.Job{ + ClusterId: "test-cluster-id", + Status: modules.JobStatusSuccess, + }, nil +} + +func (m *mockKubeFATEClient) WaitClusterUUID(string) (string, error) { + return "test-cluster-id", nil +} + +func (m *mockKubeFATEClient) IngressAddress() string { + return "test-ingress-address" +} + +func (m *mockKubeFATEClient) IngressRuleHost() string { + return "test-ingress-rule-host" +} + +func (m *mockKubeFATEClient) StopJob(string) error { + return nil +} + +var _ kubefate.Client = (*mockKubeFATEClient)(nil) // TODO: add stubs + +type mockKubeFATEManager struct { + InstallFn func() error + UninstallFn func() error + K8sClientFn func() kubernetes.Client +} + +func (m *mockKubeFATEManager) Install(bool) error { + if m.InstallFn != nil { + return m.InstallFn() + } + return nil +} + +func (m *mockKubeFATEManager) K8sClient() kubernetes.Client { + if m.K8sClientFn != nil { + return m.K8sClientFn() + } + return &mockK8sClient{} +} + +func (m *mockKubeFATEManager) Uninstall() error { + if m.UninstallFn != nil { + return m.UninstallFn() + } + return nil +} + +func (m *mockKubeFATEManager) BuildClient() (kubefate.Client, error) { + return &mockKubeFATEClient{}, nil +} + +func (m *mockKubeFATEManager) BuildPFClient() (kubefate.Client, func(), error) { + return &mockKubeFATEClient{}, nil, nil +} + +func (m *mockKubeFATEManager) InstallIngressNginxController() error { + return nil +} + +func (m *mockKubeFATEManager) GetKubeFATEDeployment() (*appv1.Deployment, error) { + return nil, nil +} + +var _ kubefate.Manager = (*mockKubeFATEManager)(nil) diff --git a/server/domain/service/mock_kubernetes_client_test.go b/server/domain/service/mock_kubernetes_client_test.go new file mode 100644 index 00000000..c288327b --- /dev/null +++ b/server/domain/service/mock_kubernetes_client_test.go @@ -0,0 +1,52 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + clientgo "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" +) + +type mockK8sClient struct { + GetClientSetFn func() clientgo.Interface + GetConfigFn func() (*rest.Config, error) + ApplyOrDeleteYAMLFn func(yamlStr string, delete bool) error +} + +func (m *mockK8sClient) GetClientSet() clientgo.Interface { + if m.GetClientSetFn != nil { + return m.GetClientSetFn() + } + return &fake.Clientset{} +} + +func (m *mockK8sClient) GetConfig() (*rest.Config, error) { + if m.GetConfigFn != nil { + return m.GetConfigFn() + } + //TODO implement me + panic("implement me") +} + +func (m *mockK8sClient) ApplyOrDeleteYAML(yamlStr string, delete bool) error { + if m.ApplyOrDeleteYAMLFn != nil { + return m.ApplyOrDeleteYAMLFn(yamlStr, delete) + } + return nil +} + +var _ kubernetes.Client = (*mockK8sClient)(nil) diff --git a/server/domain/service/mock_participant_fate_certificate_service_test.go b/server/domain/service/mock_participant_fate_certificate_service_test.go new file mode 100644 index 00000000..97da4bce --- /dev/null +++ b/server/domain/service/mock_participant_fate_certificate_service_test.go @@ -0,0 +1,65 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" +) + +type mockParticipantFATECertificateServiceInt struct { + // TODO add stubs +} + +func (m *mockParticipantFATECertificateServiceInt) DefaultCA() (*entity.CertificateAuthority, error) { + return &entity.CertificateAuthority{ + Type: 1, + ConfigurationJSON: `{"service_url":"https://127.0.0.1","service_cert_pem":"-----BEGIN CERTIFICATE-----\nMIIBpDCCAUmgAwIBAgIQYOQRhYETGs+ywmloTXJZ3TAKBggqhkjOPQQDAjAwMRIw\nEAYDVQQKEwlTbWFsbHN0ZXAxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMB4X\nDTIyMDExMTA5MTMwNloXDTMyMDEwOTA5MTMwNlowMDESMBAGA1UEChMJU21hbGxz\ndGVwMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqG\nSM49AwEHA0IABL2V5CItQTxwRzoo1pZtSQ4GeT5VzTymhv/YRJtNjjQO9PrGxA1f\nedxZg2Z/VR4imTbQafFRUDCc35PpgojXP0+jRTBDMA4GA1UdDwEB/wQEAwIBBjAS\nBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQfGCQ3WZyp9lTtZRCKBVtN/oFx\nbzAKBggqhkjOPQQDAgNJADBGAiEAr8oLkTo+Nu2gyZ9NQXQslueRSIfI2ob9A7D7\nOsrGFYYCIQCRR6APcfdvzacQ52Z8iXIpRmvYxn0tBh1VZE5y8dI09Q==\n-----END CERTIFICATE-----","provisioner_name":"admin","provisioner_password":"5nPGyRJjFPzBCVf31Oyk5NCKiTe6OwCmtDB0ZHW7"}`, + }, nil +} + +func (m *mockParticipantFATECertificateServiceInt) CreateCertificateSimple(string, time.Duration, []string) (cert *entity.Certificate, pk *rsa.PrivateKey, err error) { + cert = &entity.Certificate{ + UUID: "", + Name: "", + SerialNumberStr: "", + PEM: "", + Certificate: &x509.Certificate{ + SerialNumber: &big.Int{}, + Subject: pkix.Name{ + SerialNumber: "", + CommonName: "", + }, + DNSNames: []string{}, + }, + } + pk = &rsa.PrivateKey{} + return +} + +func (m *mockParticipantFATECertificateServiceInt) CreateBinding(*entity.Certificate, entity.CertificateBindingServiceType, string, string, entity.FederationType) error { + return nil +} + +func (m *mockParticipantFATECertificateServiceInt) RemoveBinding(string) error { + return nil +} + +var _ ParticipantCertificateServiceInt = (*mockParticipantFATECertificateServiceInt)(nil) diff --git a/server/domain/service/mock_participant_fate_endpoint_service_test.go b/server/domain/service/mock_participant_fate_endpoint_service_test.go new file mode 100644 index 00000000..4a52d1f4 --- /dev/null +++ b/server/domain/service/mock_participant_fate_endpoint_service_test.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/pkg/kubefate" +) + +type mockParticipantFATEEndpointServiceInt struct { + // TODO add stubs +} + +func (m *mockParticipantFATEEndpointServiceInt) ensureEndpointExist(string) (string, error) { + return "", nil +} + +func (m *mockParticipantFATEEndpointServiceInt) TestKubeFATE(string) error { + return nil +} + +func (m *mockParticipantFATEEndpointServiceInt) buildKubeFATEClientManagerFromEndpointUUID(string) (kubefate.ClientManager, error) { + return &mockKubeFATEManager{}, nil +} + +var _ ParticipantEndpointServiceInt = (*mockParticipantFATEEndpointServiceInt)(nil) diff --git a/server/domain/service/participant_fate_service.go b/server/domain/service/participant_fate_service.go new file mode 100644 index 00000000..5fda0c59 --- /dev/null +++ b/server/domain/service/participant_fate_service.go @@ -0,0 +1,1680 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "strings" + "sync" + "text/template" + + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/modules" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + corev1 "k8s.io/api/core/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +const ( + imagePullSecretsNameFATE = "registrykeyfate" +) + +// ParticipantFATEService is the service to manage fate participants +type ParticipantFATEService struct { + ParticipantFATERepo repo.ParticipantFATERepository + ParticipantService +} + +// ParticipantFATEExchangeYAMLCreationRequest is the request to get the exchange deployment yaml file +type ParticipantFATEExchangeYAMLCreationRequest struct { + ChartUUID string `json:"chart_uuid"` + Name string `json:"name"` + Namespace string `json:"namespace"` + ServiceType entity.ParticipantDefaultServiceType `json:"service_type"` + // RegistrySecretConfig in valueobject.KubeRegistryConfig is not used for generating the yaml content + RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config"` + EnablePSP bool `json:"enable_psp"` +} + +// ParticipantFATEClusterYAMLCreationRequest is the request to get the cluster deployment yaml +type ParticipantFATEClusterYAMLCreationRequest struct { + ParticipantFATEExchangeYAMLCreationRequest + FederationUUID string `json:"federation_uuid"` + PartyID int `json:"party_id"` + EnablePersistence bool `json:"enable_persistence"` + StorageClass string `json:"storage_class"` +} + +// ParticipantFATEExternalExchangeCreationRequest is the request for creating a record of an exchange not managed by this service +type ParticipantFATEExternalExchangeCreationRequest struct { + Name string `json:"name"` + Description string `json:"description"` + FederationUUID string `json:"federation_uuid"` + TrafficServerAccessInfo entity.ParticipantModulesAccess `json:"traffic_server_access_info"` + NginxAccessInfo entity.ParticipantModulesAccess `json:"nginx_access_info"` +} + +// ParticipantFATEExternalClusterCreationRequest is the request for creating a record of a FATE cluster not managed by this service +type ParticipantFATEExternalClusterCreationRequest struct { + Name string `json:"name"` + Description string `json:"description"` + FederationUUID string `json:"federation_uuid"` + PartyID int `json:"party_id"` + PulsarAccessInfo entity.ParticipantModulesAccess `json:"pulsar_access_info"` + NginxAccessInfo entity.ParticipantModulesAccess `json:"nginx_access_info"` +} + +type nginxRouteTableEntry struct { + Host string `json:"host"` + HttpPort int `json:"http_port"` +} + +type atsRouteTableEntry struct { + FQDN string `json:"fqdn"` + TunnelRoute string `json:"tunnelRoute"` +} + +// ParticipantFATEExchangeCreationRequest is the exchange creation request +type ParticipantFATEExchangeCreationRequest struct { + ParticipantFATEExchangeYAMLCreationRequest + ParticipantDeploymentBaseInfo + FederationUUID string `json:"federation_uuid"` + ProxyServerCertInfo entity.ParticipantComponentCertInfo `json:"proxy_server_cert_info"` + FMLManagerServerCertInfo entity.ParticipantComponentCertInfo `json:"fml_manager_server_cert_info"` + FMLManagerClientCertInfo entity.ParticipantComponentCertInfo `json:"fml_manager_client_cert_info"` +} + +// ParticipantFATEClusterCreationRequest is the cluster creation request +type ParticipantFATEClusterCreationRequest struct { + ParticipantFATEClusterYAMLCreationRequest + ParticipantDeploymentBaseInfo + PulsarServerCertInfo entity.ParticipantComponentCertInfo `json:"pulsar_server_cert_info"` + SitePortalServerCertInfo entity.ParticipantComponentCertInfo `json:"site_portal_server_cert_info"` + SitePortalClientCertInfo entity.ParticipantComponentCertInfo `json:"site_portal_client_cert_info"` +} + +// CheckPartyIDConflict returns error if the party id is taken in a federation +func (s *ParticipantFATEService) CheckPartyIDConflict(federationUUID string, partyID int) error { + conflict, err := s.ParticipantFATERepo.IsConflictedByFederationUUIDAndPartyID(federationUUID, partyID) + if err != nil { + return errors.Wrap(err, "failed to check party id existence") + } + if conflict { + return errors.Errorf("party id %v is aleady being used", partyID) + } + return nil +} + +// GetExchangeDeploymentYAML returns the exchange deployment yaml content +func (s *ParticipantFATEService) GetExchangeDeploymentYAML(req *ParticipantFATEExchangeYAMLCreationRequest) (string, error) { + instance, err := s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return "", errors.Wrapf(err, "failed to query chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeFATEExchange { + return "", errors.Errorf("chart %s is not for FATE exchange deployment", chart.UUID) + } + + t, err := template.New("fate-exchange").Parse(chart.InitialYamlTemplate) + if err != nil { + return "", err + } + + data := struct { + Name string + Namespace string + ServiceType string + UseRegistry bool + Registry string + UseImagePullSecrets bool + ImagePullSecretsName string + EnablePSP bool + }{ + Name: toDeploymentName(req.Name), + Namespace: req.Namespace, + ServiceType: req.ServiceType.String(), + UseRegistry: req.RegistryConfig.UseRegistry, + Registry: req.RegistryConfig.Registry, + UseImagePullSecrets: req.RegistryConfig.UseRegistrySecret, + ImagePullSecretsName: imagePullSecretsNameFATE, + EnablePSP: req.EnablePSP, + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// GetClusterDeploymentYAML returns a cluster deployment yaml +func (s *ParticipantFATEService) GetClusterDeploymentYAML(req *ParticipantFATEClusterYAMLCreationRequest) (string, error) { + instance, err := s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return "", errors.Wrapf(err, "failed to query chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeFATECluster { + return "", errors.Errorf("chart %s is not for FATE cluster deployment", chart.UUID) + } + + federationUUID := req.FederationUUID + instance, err = s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return "", errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationFATE) + + instance, err = s.ParticipantFATERepo.GetExchangeByFederationUUID(federationUUID) + if err != nil { + return "", errors.Wrapf(err, "failed to check exchange existence status") + } + exchange := instance.(*entity.ParticipantFATE) + + if exchange.Status != entity.ParticipantFATEStatusActive { + return "", errors.Errorf("exchange %v is not in active status", exchange.UUID) + } + + accessInfoMap := exchange.AccessInfo + if accessInfoMap == nil { + return "", errors.New("exchange access info is missing") + } + + data := struct { + Name string + Namespace string + PartyID int + ExchangeNginxHost string + ExchangeNginxPort int + ExchangeATSHost string + ExchangeATSPort int + Domain string + ServiceType string + UseRegistry bool + Registry string + UseImagePullSecrets bool + ImagePullSecretsName string + SitePortalTLSCommonName string + EnablePersistence bool + StorageClass string + EnablePSP bool + }{ + Name: toDeploymentName(req.Name), + Namespace: req.Namespace, + PartyID: req.PartyID, + Domain: federation.Domain, + ServiceType: req.ServiceType.String(), + UseRegistry: req.RegistryConfig.UseRegistry, + Registry: req.RegistryConfig.Registry, + UseImagePullSecrets: req.RegistryConfig.UseRegistrySecret, + ImagePullSecretsName: imagePullSecretsNameFATE, + SitePortalTLSCommonName: fmt.Sprintf("site-%d.server.%s", req.PartyID, federation.Domain), + EnablePersistence: req.EnablePersistence, + StorageClass: req.StorageClass, + EnablePSP: req.EnablePSP, + } + if nginxAccess, ok := accessInfoMap[entity.ParticipantFATEServiceNameNginx]; !ok { + return "", errors.New("missing exchange nginx access info") + } else { + data.ExchangeNginxHost = nginxAccess.Host + data.ExchangeNginxPort = nginxAccess.Port + } + if atsAccess, ok := accessInfoMap[entity.ParticipantFATEServiceNameATS]; !ok { + return "", errors.New("missing exchange traffic-server access info") + } else { + data.ExchangeATSHost = atsAccess.Host + data.ExchangeATSPort = atsAccess.Port + } + + t, err := template.New("fate-cluster").Parse(chart.InitialYamlTemplate) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// CreateExchange creates a FATE exchange, the returned *sync.WaitGroup can be used to wait for the completion of the async goroutine +func (s *ParticipantFATEService) CreateExchange(req *ParticipantFATEExchangeCreationRequest) (*entity.ParticipantFATE, *sync.WaitGroup, error) { + federationUUID := req.FederationUUID + instance, err := s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationFATE) + + if exist, err := s.ParticipantFATERepo.IsExchangeCreatedByFederationUUID(federationUUID); err != nil { + return nil, nil, errors.Wrapf(err, "failed to check exchange existence status") + } else if exist { + return nil, nil, errors.Errorf("an exchange is already deployed in federation %s", federationUUID) + } + + if err := s.EndpointService.TestKubeFATE(req.EndpointUUID); err != nil { + return nil, nil, err + } + + instance, err = s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return nil, nil, errors.Wrapf(err, "faile to get chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeFATEExchange { + return nil, nil, errors.Errorf("chart %s is not for FATE exchange deployment", chart.UUID) + } + + var m map[string]interface{} + err = yaml.Unmarshal([]byte(req.DeploymentYAML), &m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal deployment yaml") + } + + m["name"] = toDeploymentName(req.Name) + m["namespace"] = req.Namespace + + finalYAMLBytes, err := yaml.Marshal(m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get final yaml content") + } + req.DeploymentYAML = string(finalYAMLBytes) + + if req.ProxyServerCertInfo.BindingMode == entity.CertBindingModeReuse { + return nil, nil, errors.New("cannot re-use existing certificate") + } + + var caCert *x509.Certificate + if req.ProxyServerCertInfo.BindingMode == entity.CertBindingModeCreate || + req.FMLManagerServerCertInfo.BindingMode == entity.CertBindingModeCreate || + req.FMLManagerClientCertInfo.BindingMode == entity.CertBindingModeCreate { + ca, err := s.CertificateService.DefaultCA() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get default CA") + } + caCert, err = ca.RootCert() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get CA cert") + } + } + + exchange := &entity.ParticipantFATE{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.FederationUUID, + EndpointUUID: req.EndpointUUID, + ChartUUID: req.ChartUUID, + Namespace: req.Namespace, + DeploymentYAML: req.DeploymentYAML, + IsManaged: true, + ExtraAttribute: entity.ParticipantExtraAttribute{ + IsNewNamespace: false, + UseRegistrySecret: req.RegistryConfig.UseRegistrySecret, + }, + }, + Type: entity.ParticipantFATETypeExchange, + PartyID: 0, + Status: entity.ParticipantFATEStatusInstalling, + AccessInfo: entity.ParticipantFATEModulesAccessMap{}, + CertConfig: entity.ParticipantFATECertConfig{ + ProxyServerCertInfo: req.ProxyServerCertInfo, + FMLManagerClientCertInfo: req.FMLManagerClientCertInfo, + FMLManagerServerCertInfo: req.FMLManagerServerCertInfo, + }, + } + err = s.ParticipantFATERepo.Create(exchange) + if err != nil { + return nil, nil, err + } + + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, "start creating exchange", entity.EventLogLevelInfo) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "installing fate exchange").Str("uuid", exchange.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("creating FATE exchange %s with UUID %s", exchange.Name, exchange.UUID) + if err := func() error { + endpointMgr, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(req.EndpointUUID) + if kfClientCloser != nil { + defer kfClientCloser() + } + if err != nil { + return err + } + + if chart.Private { + operationLog.Info().Msgf("making sure the chart is uploaded, name: %s, version: %s", chart.ChartName, chart.Version) + if err := kfClient.EnsureChartExist(chart.ChartName, chart.Version, chart.ArchiveContent); err != nil { + return errors.Wrapf(err, "error uploading FedLCM private chart") + } + } + + if exchange.ExtraAttribute.IsNewNamespace, err = ensureNSExisting(endpointMgr.K8sClient(), req.Namespace); err != nil { + return err + } + if exchange.ExtraAttribute.IsNewNamespace { + operationLog.Info().Msgf("created new namespace: %s", req.Namespace) + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange attribute") + } + } else { + if clusters, err := kfClient.ListClusterByNamespace(req.Namespace); err != nil { + return errors.Wrap(err, "failed to check existing fate or exchange installation") + } else if len(clusters) > 0 { + return errors.Errorf("cannot add new exchange as existing installations are running in the same namespace: %v", *clusters[0]) + } + } + + if req.RegistryConfig.UseRegistrySecret { + if err := createRegistrySecret(endpointMgr.K8sClient(), imagePullSecretsNameFATE, req.Namespace, req.RegistryConfig.RegistrySecretConfig); err != nil { + return errors.Wrap(err, "failed to create registry secret") + } else { + operationLog.Info().Msgf("created registry secret %s with username %s for URL %s", imagePullSecretsNameFATE, req.RegistryConfig.RegistrySecretConfig.Username, req.RegistryConfig.RegistrySecretConfig.ServerURL) + } + } + + atsFQDN := req.ProxyServerCertInfo.CommonName + if atsFQDN == "" { + atsFQDN = fmt.Sprintf("proxy.%s", federation.Domain) + } + if req.ProxyServerCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.ProxyServerCertInfo.CommonName == "" { + req.ProxyServerCertInfo.CommonName = atsFQDN + } + operationLog.Info().Msgf("creating certificate for the ATS service with CN: %s", req.ProxyServerCertInfo.CommonName) + dnsNames := []string{req.ProxyServerCertInfo.CommonName} + cert, pk, err := s.CertificateService.CreateCertificateSimple(req.ProxyServerCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create ATS certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", cert.SerialNumber, cert.Subject.CommonName) + err = createATSSecret(endpointMgr.K8sClient(), req.Namespace, caCert, cert, pk) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(cert, entity.CertificateBindingServiceTypeATS, exchange.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + exchange.CertConfig.ProxyServerCertInfo.CommonName = req.ProxyServerCertInfo.CommonName + exchange.CertConfig.ProxyServerCertInfo.UUID = cert.UUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange cert info") + } + } + + fmlManagerFQDN := req.FMLManagerServerCertInfo.CommonName + if fmlManagerFQDN == "" { + fmlManagerFQDN = fmt.Sprintf("fmlmanager.server.%s", federation.Domain) + } + var serverCert, clientCert *entity.Certificate + var serverPrivateKey, clientPrivateKey *rsa.PrivateKey + if req.FMLManagerServerCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.FMLManagerServerCertInfo.CommonName == "" { + req.FMLManagerServerCertInfo.CommonName = fmt.Sprintf("fmlmanager.server.%s", federation.Domain) + } + operationLog.Info().Msgf("creating certificate for FML Manager server with CN: %s", req.FMLManagerServerCertInfo.CommonName) + dnsNames := []string{req.FMLManagerServerCertInfo.CommonName, "localhost"} + serverCert, serverPrivateKey, err = s.CertificateService.CreateCertificateSimple(req.FMLManagerServerCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create FML Manager server certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", serverCert.SerialNumber, serverCert.Subject.CommonName) + } + if req.FMLManagerClientCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.FMLManagerClientCertInfo.CommonName == "" { + req.FMLManagerClientCertInfo.CommonName = fmt.Sprintf("fmlmanager.client.%s", federation.Domain) + } + operationLog.Info().Msgf("creating certificate for FML Manager client with CN: %s", req.FMLManagerClientCertInfo.CommonName) + dnsNames := []string{req.FMLManagerClientCertInfo.CommonName} + clientCert, clientPrivateKey, err = s.CertificateService.CreateCertificateSimple(req.FMLManagerClientCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create FML Manager client certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", clientCert.SerialNumber, clientCert.Subject.CommonName) + } + if req.FMLManagerServerCertInfo.BindingMode != entity.CertBindingModeSkip && req.FMLManagerClientCertInfo.BindingMode != entity.CertBindingModeSkip { + err = createTLSSecret(endpointMgr.K8sClient(), req.Namespace, serverCert, serverPrivateKey, clientCert, clientPrivateKey, caCert, entity.ParticipantFATESecretNameFMLMgr) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(serverCert, entity.CertificateBindingServiceFMLManagerServer, exchange.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + exchange.CertConfig.FMLManagerServerCertInfo.CommonName = req.FMLManagerServerCertInfo.CommonName + exchange.CertConfig.FMLManagerServerCertInfo.UUID = serverCert.UUID + if err := s.CertificateService.CreateBinding(clientCert, entity.CertificateBindingServiceFMLManagerClient, exchange.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + exchange.CertConfig.FMLManagerClientCertInfo.CommonName = req.FMLManagerClientCertInfo.CommonName + exchange.CertConfig.FMLManagerClientCertInfo.UUID = clientCert.UUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange cert info") + } + } + + jobUUID, err := kfClient.SubmitClusterInstallationJob(exchange.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "fail to submit cluster creation request") + } + exchange.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange's job uuid") + } + operationLog.Info().Msgf("kubefate job created, uuid: %s", exchange.JobUUID) + clusterUUID, err := kfClient.WaitClusterUUID(jobUUID) + if err != nil { + return errors.Wrapf(err, "fail to get cluster uuid") + } + exchange.ClusterUUID = clusterUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange cluster uuid") + } + operationLog.Info().Msgf("kubefate-managed cluster created, uuid: %s", exchange.ClusterUUID) + + job, err := kfClient.WaitJob(jobUUID) + if err != nil { + return err + } + if job.Status != modules.JobStatusSuccess { + return errors.Errorf("job is %s, job info: %v", job.Status.String(), job) + } + operationLog.Info().Msgf("kubefate job succeeded") + + serviceType, host, port, err := getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNameNginx), "http") + if err != nil { + return errors.Wrapf(err, "fail to get nginx access info") + } + exchange.AccessInfo[entity.ParticipantFATEServiceNameNginx] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: false, + } + + serviceType, host, port, err = getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNameATS), "443") + if err != nil { + return errors.Wrapf(err, "fail to get ATS access info") + } + exchange.AccessInfo[entity.ParticipantFATEServiceNameATS] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: atsFQDN, + } + + moduleList := m["modules"].([]interface{}) + for _, moduleName := range moduleList { + if moduleName.(string) == "fmlManagerServer" { + operationLog.Info().Msgf("fml-manager deployed, retrieving access info") + serviceType, host, port, err := getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNameFMLMgr), "https-fml-manager-server") + if err != nil { + return errors.Wrapf(err, "fail to get fml manager access info") + } + exchange.AccessInfo[entity.ParticipantFATEServiceNameFMLMgr] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: fmlManagerFQDN, + } + } + } + + exchange.Status = entity.ParticipantFATEStatusActive + if err := s.BuildIngressInfoMap(exchange); err != nil { + return errors.Wrapf(err, "failed to get ingress info") + } + return s.ParticipantFATERepo.UpdateInfoByUUID(exchange) + }(); err != nil { + operationLog.Error().Msgf(errors.Wrapf(err, "failed to install FATE exchange").Error()) + exchange.Status = entity.ParticipantFATEStatusFailed + if updateErr := s.ParticipantFATERepo.UpdateStatusByUUID(exchange); updateErr != nil { + operationLog.Error().Msgf(errors.Wrapf(updateErr, "failed to update FATE exchange status").Error()) + } + return + } + operationLog.Info().Msgf("FATE exchange %s(%s) deployed", exchange.Name, exchange.UUID) + }() + + return exchange, wg, nil +} + +// RemoveExchange removes and uninstalls a FATE exchange +func (s *ParticipantFATEService) RemoveExchange(uuid string, force bool) (*sync.WaitGroup, error) { + exchange, err := s.loadParticipant(uuid) + if err != nil { + return nil, err + } + if exchange.Type != entity.ParticipantFATETypeExchange { + return nil, errors.Errorf("participant %s is not a FATE exchange", exchange.UUID) + } + + if !force && exchange.Status != entity.ParticipantFATEStatusActive { + return nil, errors.Errorf("exchange cannot be removed when in status: %v", exchange.Status) + } + + instanceList, err := s.ParticipantFATERepo.ListByFederationUUID(exchange.FederationUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to list participants in federation") + } + participantList := instanceList.([]entity.ParticipantFATE) + if len(participantList) > 1 { + return nil, errors.Errorf("cannot remove exchange as there are %v cluster(s) in this federation", len(participantList)-1) + } + + exchange.Status = entity.ParticipantFATEStatusRemoving + if err := s.ParticipantFATERepo.UpdateStatusByUUID(exchange); err != nil { + return nil, errors.Wrapf(err, "failed to update exchange status") + } + + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, "start removing exchange", entity.EventLogLevelInfo) + + // just do db deletion for unmanaged exchange + if !exchange.IsManaged { + if err := s.ParticipantFATERepo.DeleteByUUID(exchange.UUID); err != nil { + log.Err(err).Msg("delete external exchange error:") + return nil, err + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, "removed external exchange", entity.EventLogLevelInfo) + return nil, nil + } + // TODO: revoke the certificate when we have some OCSP mechanism in place + if err := s.CertificateService.RemoveBinding(exchange.UUID); err != nil { + return nil, errors.Wrapf(err, "failed to remove certificate bindings") + } + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "uninstalling fate exchange").Str("uuid", exchange.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("uninstalling FATE exchange %s with UUID %s", exchange.Name, exchange.UUID) + err := func() error { + endpointMgr, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(exchange.EndpointUUID) + if kfClientCloser != nil { + defer kfClientCloser() + } + if err != nil { + return err + } + if exchange.JobUUID != "" { + operationLog.Info().Msgf("stopping KubeFATE job %s", exchange.JobUUID) + err := kfClient.StopJob(exchange.JobUUID) + if err != nil { + return err + } + } + if exchange.ClusterUUID != "" { + operationLog.Info().Msgf("deleting KubeFATE-managed cluster %s", exchange.ClusterUUID) + jobUUID, err := kfClient.SubmitClusterDeletionJob(exchange.ClusterUUID) + if err != nil { + // TODO: use helm or client-go to try to clean things up + return err + } + if jobUUID != "" { + operationLog.Info().Msgf("deleting job UUID %s", jobUUID) + exchange.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange job uuid") + } + if job, err := kfClient.WaitJob(jobUUID); err != nil { + return err + } else if job.Status != modules.JobStatusSuccess { + operationLog.Warn().Msgf("deleting job not succeeded, status: %v, job info: %v", job.Status, job) + } + } + } + + if exchange.ExtraAttribute.UseRegistrySecret { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(exchange.Namespace). + Delete(context.TODO(), imagePullSecretsNameFATE, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error deleting registry secret").Error()) + } else { + operationLog.Info().Msgf("deleted registry secret %s", imagePullSecretsNameFATE) + } + } + if exchange.CertConfig.ProxyServerCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(exchange.Namespace). + Delete(context.TODO(), entity.ParticipantFATESecretNameATS, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msg(errors.Wrapf(err, "error deleting stale cert secret: %s", entity.ParticipantFATESecretNameATS).Error()) + } else { + operationLog.Info().Msgf("deleted stale cert secret: %s", entity.ParticipantFATESecretNameATS) + } + } + + if exchange.CertConfig.FMLManagerServerCertInfo.BindingMode != entity.CertBindingModeSkip && exchange.CertConfig.FMLManagerClientCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(exchange.Namespace). + Delete(context.TODO(), entity.ParticipantFATESecretNameFMLMgr, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msg(errors.Wrapf(err, "error deleting stale %s secret", entity.ParticipantFATESecretNameFMLMgr).Error()) + } else { + operationLog.Info().Msgf("deleted stale %s secret", entity.ParticipantFATESecretNameFMLMgr) + } + } + if exchange.ExtraAttribute.IsNewNamespace { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Namespaces().Delete(context.TODO(), exchange.Namespace, v1.DeleteOptions{}); err != nil && !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete namespace") + } + operationLog.Info().Msgf("namespace %s deleted", exchange.Namespace) + } + return nil + }() + if err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error uninstalling exchange").Error()) + if !force { + return + } + } + if deleteErr := s.ParticipantFATERepo.DeleteByUUID(exchange.UUID); deleteErr != nil { + operationLog.Error().Msgf(errors.Wrap(err, "error deleting exchange from repo").Error()) + return + } + operationLog.Info().Msgf("uninstalled FATE exchange %s with UUID %s", exchange.Name, exchange.UUID) + }() + return wg, nil +} + +// CreateExternalExchange creates an external FATE exchange with the access info provided by user +func (s *ParticipantFATEService) CreateExternalExchange(req *ParticipantFATEExternalExchangeCreationRequest) (*entity.ParticipantFATE, error) { + federationUUID := req.FederationUUID + if exist, err := s.ParticipantFATERepo.IsExchangeCreatedByFederationUUID(federationUUID); err != nil { + return nil, errors.Wrapf(err, "failed to check exchange existence status") + } else if exist { + return nil, errors.Errorf("an exchange is already existed in federation %s", federationUUID) + } + instance, err := s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return nil, errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationFATE) + + exchange := &entity.ParticipantFATE{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.FederationUUID, + EndpointUUID: "Unknown", + ChartUUID: "Unknown", + Namespace: "Unknown", + ClusterUUID: "Unknown", + DeploymentYAML: "Unknown", + IsManaged: false, + }, + Type: entity.ParticipantFATETypeExchange, + PartyID: 0, + Status: entity.ParticipantFATEStatusActive, + AccessInfo: entity.ParticipantFATEModulesAccessMap{}, + } + exchange.AccessInfo[entity.ParticipantFATEServiceNameATS] = entity.ParticipantModulesAccess{ + ServiceType: corev1.ServiceType(entity.ParticipantDefaultServiceTypeNodePort.String()), + Host: req.TrafficServerAccessInfo.Host, + Port: req.TrafficServerAccessInfo.Port, + TLS: true, + FQDN: "proxy." + federation.Domain, + } + exchange.AccessInfo[entity.ParticipantFATEServiceNameNginx] = entity.ParticipantModulesAccess{ + ServiceType: corev1.ServiceType(entity.ParticipantDefaultServiceTypeNodePort.String()), + Host: req.NginxAccessInfo.Host, + Port: req.NginxAccessInfo.Port, + TLS: false, + FQDN: "", + } + + if err := s.ParticipantFATERepo.Create(exchange); err != nil { + return nil, err + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, "created an external exchange", entity.EventLogLevelInfo) + return exchange, nil +} + +// CreateCluster creates a FATE cluster with exchange's access info, and will update exchange's route table +func (s *ParticipantFATEService) CreateCluster(req *ParticipantFATEClusterCreationRequest) (*entity.ParticipantFATE, *sync.WaitGroup, error) { + if err := s.CheckPartyIDConflict(req.FederationUUID, req.PartyID); err != nil { + return nil, nil, err + } + federationUUID := req.FederationUUID + instance, err := s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationFATE) + + instance, err = s.ParticipantFATERepo.GetExchangeByFederationUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to check exchange existence status") + } + exchange := instance.(*entity.ParticipantFATE) + + if exchange.Status != entity.ParticipantFATEStatusActive { + return nil, nil, errors.Errorf("exchange %v is not in active status", exchange.UUID) + } + if exchange.IsManaged { + if err := s.EndpointService.TestKubeFATE(exchange.EndpointUUID); err != nil { + return nil, nil, err + } + } + + if err := s.EndpointService.TestKubeFATE(req.EndpointUUID); err != nil { + return nil, nil, err + } + + instance, err = s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return nil, nil, errors.Wrapf(err, "faile to get chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeFATECluster { + return nil, nil, errors.Errorf("chart %s is not for FATE cluster deployment", chart.UUID) + } + + var m map[string]interface{} + err = yaml.Unmarshal([]byte(req.DeploymentYAML), &m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal deployment yaml") + } + + m["name"] = toDeploymentName(req.Name) + m["namespace"] = req.Namespace + + finalYAMLBytes, err := yaml.Marshal(m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get final yaml content") + } + req.DeploymentYAML = string(finalYAMLBytes) + + pulsarDomain, err := getPulsarDomainFromYAML(req.DeploymentYAML) + if err != nil { + return nil, nil, err + } + if pulsarDomain == "" { + log.Warn().Msgf("using federation domain") + pulsarDomain = federation.Domain + } + pulsarFQDN := fmt.Sprintf("%d.%s", req.PartyID, pulsarDomain) + + var caCert *x509.Certificate + if req.PulsarServerCertInfo.BindingMode == entity.CertBindingModeCreate || + req.SitePortalClientCertInfo.BindingMode == entity.CertBindingModeCreate || + req.SitePortalServerCertInfo.BindingMode == entity.CertBindingModeCreate { + ca, err := s.CertificateService.DefaultCA() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get default CA") + } + caCert, err = ca.RootCert() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get CA cert") + } + } + + cluster := &entity.ParticipantFATE{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.FederationUUID, + EndpointUUID: req.EndpointUUID, + ChartUUID: req.ChartUUID, + Namespace: req.Namespace, + DeploymentYAML: req.DeploymentYAML, + IsManaged: true, + ExtraAttribute: entity.ParticipantExtraAttribute{ + IsNewNamespace: false, + UseRegistrySecret: req.RegistryConfig.UseRegistrySecret, + }, + }, + Type: entity.ParticipantFATETypeCluster, + PartyID: req.PartyID, + Status: entity.ParticipantFATEStatusInstalling, + AccessInfo: entity.ParticipantFATEModulesAccessMap{}, + CertConfig: entity.ParticipantFATECertConfig{ + PulsarServerCertInfo: req.PulsarServerCertInfo, + SitePortalServerCertInfo: req.SitePortalServerCertInfo, + SitePortalClientCertInfo: req.SitePortalClientCertInfo, + }, + } + err = s.ParticipantFATERepo.Create(cluster) + if err != nil { + return nil, nil, err + } + + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "start creating cluster", entity.EventLogLevelInfo) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "installing fate cluster").Str("uuid", cluster.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("creating FATE cluster %s with UUID %s", cluster.Name, cluster.UUID) + if err := func() error { + endpointMgr, kfClient, closer, err := s.buildKubeFATEMgrAndClient(req.EndpointUUID) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + if chart.Private { + operationLog.Info().Msgf("making sure the chart is uploaded, name: %s, version: %s", chart.ChartName, chart.Version) + if err := kfClient.EnsureChartExist(chart.ChartName, chart.Version, chart.ArchiveContent); err != nil { + return errors.Wrapf(err, "error uploading FedLCM private chart") + } + } + + if cluster.ExtraAttribute.IsNewNamespace, err = ensureNSExisting(endpointMgr.K8sClient(), req.Namespace); err != nil { + return err + } + if cluster.ExtraAttribute.IsNewNamespace { + operationLog.Info().Msgf("created new namespace: %s for this cluster", req.Namespace) + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster attribute") + } + } else { + if clusters, err := kfClient.ListClusterByNamespace(req.Namespace); err != nil { + return errors.Wrap(err, "failed to check existing fate or exchange installation") + } else if len(clusters) > 0 { + return errors.Errorf("cannot add new fate cluster as existing installations are running in the same namespace: %v", *clusters[0]) + } + } + + if req.RegistryConfig.UseRegistrySecret { + if err := createRegistrySecret(endpointMgr.K8sClient(), imagePullSecretsNameFATE, req.Namespace, req.RegistryConfig.RegistrySecretConfig); err != nil { + return errors.Wrap(err, "failed to create registry secret") + } else { + operationLog.Info().Msgf("created registry secret %s with username %s for URL %s", imagePullSecretsNameFATE, req.RegistryConfig.RegistrySecretConfig.Username, req.RegistryConfig.RegistrySecretConfig.ServerURL) + } + } + + if req.PulsarServerCertInfo.BindingMode == entity.CertBindingModeCreate { + req.PulsarServerCertInfo.CommonName = fmt.Sprintf("%v.%s", req.PartyID, federation.Domain) + operationLog.Info().Msgf("creating certificate for the pulsar service with CN: %s", req.PulsarServerCertInfo.CommonName) + dnsNames := []string{req.PulsarServerCertInfo.CommonName} + cert, pk, err := s.CertificateService.CreateCertificateSimple(req.PulsarServerCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create pulsar certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", cert.SerialNumber, cert.Subject.CommonName) + err = createPulsarSecret(endpointMgr.K8sClient(), req.Namespace, caCert, cert, pk) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(cert, entity.CertificateBindingServiceTypePulsarServer, cluster.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + cluster.CertConfig.PulsarServerCertInfo.CommonName = req.PulsarServerCertInfo.CommonName + cluster.CertConfig.PulsarServerCertInfo.UUID = cert.UUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster cert info") + } + } + + sitePortalFQDN := fmt.Sprintf("site-%v.server.%s", req.PartyID, federation.Domain) + var serverCert, clientCert *entity.Certificate + var serverPrivateKey, clientPrivateKey *rsa.PrivateKey + if req.SitePortalServerCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.SitePortalServerCertInfo.CommonName == "" { + req.SitePortalServerCertInfo.CommonName = fmt.Sprintf("site-%v.server.%s", req.PartyID, federation.Domain) + } + operationLog.Info().Msgf("creating certificate for Site Portal server with CN: %s", req.SitePortalServerCertInfo.CommonName) + // we need localhost in the dnsNames because the site portal will call itself during some workflows + dnsNames := []string{req.SitePortalServerCertInfo.CommonName, "localhost"} + serverCert, serverPrivateKey, err = s.CertificateService.CreateCertificateSimple(req.SitePortalServerCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create Site Portal server certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", serverCert.SerialNumber, serverCert.Subject.CommonName) + } + if req.SitePortalClientCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.SitePortalClientCertInfo.CommonName == "" { + req.SitePortalClientCertInfo.CommonName = fmt.Sprintf("site-%v.client.%s", req.PartyID, federation.Domain) + } + operationLog.Info().Msgf("creating certificate for Site Portal client with CN: %s", req.SitePortalClientCertInfo.CommonName) + dnsNames := []string{req.SitePortalClientCertInfo.CommonName} + clientCert, clientPrivateKey, err = s.CertificateService.CreateCertificateSimple(req.SitePortalClientCertInfo.CommonName, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create Site Portal client certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", clientCert.SerialNumber, clientCert.Subject.CommonName) + } + if req.SitePortalServerCertInfo.BindingMode != entity.CertBindingModeSkip && req.SitePortalClientCertInfo.BindingMode != entity.CertBindingModeSkip { + err = createTLSSecret(endpointMgr.K8sClient(), req.Namespace, serverCert, serverPrivateKey, clientCert, clientPrivateKey, caCert, entity.ParticipantFATESecretNamePortal) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(serverCert, entity.CertificateBindingServiceSitePortalServer, cluster.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + cluster.CertConfig.SitePortalServerCertInfo.CommonName = req.SitePortalServerCertInfo.CommonName + cluster.CertConfig.SitePortalServerCertInfo.UUID = serverCert.UUID + if err := s.CertificateService.CreateBinding(clientCert, entity.CertificateBindingServiceSitePortalClient, cluster.UUID, federationUUID, entity.FederationTypeFATE); err != nil { + return err + } + cluster.CertConfig.SitePortalClientCertInfo.CommonName = req.SitePortalClientCertInfo.CommonName + cluster.CertConfig.SitePortalClientCertInfo.UUID = clientCert.UUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster cert info") + } + operationLog.Info().Msgf("created cert secret and bindings for site-portal service") + } + + // TODO: ensure there is no same-name cluster exists because we may create cluster-level resources using the cluster name + jobUUID, err := kfClient.SubmitClusterInstallationJob(cluster.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "fail to submit cluster creation request") + } + cluster.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster's job uuid") + } + operationLog.Info().Msgf("kubefate job created, uuid: %s", cluster.JobUUID) + clusterUUID, err := kfClient.WaitClusterUUID(jobUUID) + if err != nil { + return errors.Wrapf(err, "fail to get cluster uuid") + } + cluster.ClusterUUID = clusterUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster uuid") + } + operationLog.Info().Msgf("kubefate-managed cluster created, uuid: %s", cluster.ClusterUUID) + job, err := kfClient.WaitJob(jobUUID) + if err != nil { + return err + } + if job.Status != modules.JobStatusSuccess { + return errors.Errorf("job is %s, job info: %v", job.Status.String(), job) + } + + serviceType, host, port, err := getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNameNginx), "http") + if err != nil { + return errors.Wrapf(err, "fail to get nginx access info") + } + cluster.AccessInfo[entity.ParticipantFATEServiceNameNginx] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: false, + } + operationLog.Info().Msgf("kubefate job succeeded") + + // the pulsar-public-tls service is always of type LoadBalancer, we try to use nodePort if no LoadBalancer IP is available + serviceType, host, port, err = getServiceAccessWithFallback(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNamePulsar), "tls-port", true) + if err != nil { + return errors.Wrapf(err, "fail to get pulsar access info") + } + cluster.AccessInfo[entity.ParticipantFATEServiceNamePulsar] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: pulsarFQDN, + } + + moduleList := m["modules"].([]interface{}) + for _, moduleName := range moduleList { + if moduleName.(string) == "frontend" { + operationLog.Info().Msgf("site-portal deployed, retrieving access info") + serviceType, host, port, err := getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantFATEServiceNamePortal), "https-frontend") + if err != nil { + return errors.Wrapf(err, "fail to get site portal access info") + } + cluster.AccessInfo[entity.ParticipantFATEServiceNamePortal] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: sitePortalFQDN, + } + } + } + + cluster.Status = entity.ParticipantFATEStatusActive + if err := s.BuildIngressInfoMap(cluster); err != nil { + return errors.Wrapf(err, "failed to get ingress info") + } + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to save cluster info") + } + if exchange.IsManaged { + operationLog.Info().Msg("rebuilding exchange route table") + if err := s.rebuildRouteTable(exchange); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error rebuilding route table while creating cluster").Error()) + } + } + return nil + }(); err != nil { + operationLog.Error().Msgf(errors.Wrap(err, "failed to install FATE cluster").Error()) + cluster.Status = entity.ParticipantFATEStatusFailed + if updateErr := s.ParticipantFATERepo.UpdateStatusByUUID(cluster); updateErr != nil { + operationLog.Error().Msgf(errors.Wrap(err, "failed to update FATE cluster status").Error()) + } + return + } + operationLog.Info().Msgf("FATE cluster %s(%s) deployed", cluster.Name, cluster.UUID) + }() + return cluster, wg, nil +} + +// RemoveCluster uninstall the cluster as well as remove it from the exchange's route table +func (s *ParticipantFATEService) RemoveCluster(uuid string, force bool) (*sync.WaitGroup, error) { + cluster, err := s.loadParticipant(uuid) + if err != nil { + return nil, err + } + if cluster.Type != entity.ParticipantFATETypeCluster { + return nil, errors.Errorf("participant %s is not a FATE cluster", cluster.UUID) + } + + if !force && cluster.Status != entity.ParticipantFATEStatusActive { + return nil, errors.Errorf("cluster cannot be removed when in status: %v", cluster.Status) + } + + instance, err := s.ParticipantFATERepo.GetExchangeByFederationUUID(cluster.FederationUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to check exchange existence status") + } + exchange := instance.(*entity.ParticipantFATE) + + if exchange.IsManaged { + if err := s.EndpointService.TestKubeFATE(exchange.EndpointUUID); err != nil { + if !force { + return nil, err + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "failed to test exchange endpoint connection, continue as the force flag is set", entity.EventLogLevelError) + } + } + if cluster.IsManaged { + if err := s.EndpointService.TestKubeFATE(cluster.EndpointUUID); err != nil { + if !force { + return nil, err + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "failed to test cluster endpoint connection, continue as the force flag is set", entity.EventLogLevelError) + } + } + + cluster.Status = entity.ParticipantFATEStatusRemoving + if err := s.ParticipantFATERepo.UpdateStatusByUUID(cluster); err != nil { + return nil, errors.Wrapf(err, "failed to update cluster status") + } + + if cluster.IsManaged { + if err := s.CertificateService.RemoveBinding(cluster.UUID); err != nil { + return nil, errors.Wrapf(err, "failed to remove certificate bindings") + } + } + + err = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "start removing cluster", entity.EventLogLevelInfo) + if err != nil { + return nil, err + } + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "uninstalling fate cluster").Str("uuid", cluster.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("uninstalling FATE cluster %s with UUID %s", cluster.Name, cluster.UUID) + err := func() error { + if exchange.IsManaged { + operationLog.Info().Msgf("updating exchange route table") + if err := func() error { + exchangeEndpointMgr, exchangeKFClient, exchangeKFClientCloser, err := s.buildKubeFATEMgrAndClient(exchange.EndpointUUID) + if exchangeKFClientCloser != nil { + defer exchangeKFClientCloser() + } + if err != nil { + return errors.Wrap(err, "cannot get exchange endpoint manager") + } + + // this process is faster than the rebuildRouteTable function + var m map[string]interface{} + _ = yaml.Unmarshal([]byte(exchange.DeploymentYAML), &m) + + if m["nginx"].(map[string]interface{})["route_table"] != nil { + routeTable := m["nginx"].(map[string]interface{})["route_table"].(map[string]interface{}) + delete(routeTable, fmt.Sprintf("%v", cluster.PartyID)) + } + + sniTable := m["trafficServer"].(map[string]interface{})["route_table"].(map[string]interface{})["sni"] + if sniTable != nil { + var newTable []interface{} + + sniTableList := sniTable.([]interface{}) + for _, item := range sniTableList { + itemBytes, _ := yaml.Marshal(item) + var entry atsRouteTableEntry + _ = yaml.Unmarshal(itemBytes, &entry) + if strings.HasPrefix(entry.FQDN, fmt.Sprintf("%v.", cluster.PartyID)) { + continue + } + newTable = append(newTable, item) + } + m["trafficServer"].(map[string]interface{})["route_table"].(map[string]interface{})["sni"] = newTable + } + + updatedYaml, _ := yaml.Marshal(m) + + var originalMap map[string]interface{} + _ = yaml.Unmarshal([]byte(exchange.DeploymentYAML), &originalMap) + originalYaml, _ := yaml.Marshal(originalMap) + if bytes.Equal(updatedYaml, originalYaml) { + operationLog.Info().Msg("exchange yaml not changed") + return nil + } + + exchange.DeploymentYAML = string(updatedYaml) + if err := s.ParticipantFATERepo.UpdateDeploymentYAMLByUUID(exchange); err != nil { + return errors.Wrapf(err, "failed to update exchange info") + } + + jobUUID, err := exchangeKFClient.SubmitClusterUpdateJob(exchange.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "failed to submit exchange update job") + } + exchange.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange's job uuid") + } + if job, err := exchangeKFClient.WaitJob(jobUUID); err != nil { + return errors.Wrapf(err, "failed to query exchange update job status") + } else if job.Status != modules.JobStatusSuccess { + operationLog.Warn().Msgf("updating job not succeeded, status: %v, job info: %v", job.Status, job) + } + operationLog.Info().Msgf("restarting exchange ATS") + return deletePodWithPrefix(exchangeEndpointMgr.K8sClient(), exchange.Namespace, "traffic-server") + }(); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error updating exchange route table").Error()) + if err := s.rebuildRouteTable(exchange); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error rebuilding route table while deleting cluster").Error()) + } else { + operationLog.Info().Msg("successfully rebuild route table") + } + } + } + if cluster.IsManaged { + clusterEndpointMgr, clusterKFClient, clusterKFClientCloser, err := s.buildKubeFATEMgrAndClient(cluster.EndpointUUID) + if clusterKFClientCloser != nil { + defer clusterKFClientCloser() + } + if err != nil { + return errors.Wrap(err, "cannot get cluster endpoint manager") + } + if cluster.JobUUID != "" { + operationLog.Info().Msgf("stopping KubeFATE job %s", cluster.JobUUID) + err := clusterKFClient.StopJob(cluster.JobUUID) + if err != nil { + return err + } + } + if cluster.ClusterUUID != "" { + operationLog.Info().Msgf("deleting KubeFATE-managed cluster %s", cluster.ClusterUUID) + jobUUID, err := clusterKFClient.SubmitClusterDeletionJob(cluster.ClusterUUID) + if err != nil { + // TODO: use helm or client-go to try to clean things up + return err + } + if jobUUID != "" { + operationLog.Info().Msgf("deleting job UUID %s", jobUUID) + cluster.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil { + return errors.Wrap(err, "failed to update cluster's job uuid") + } + if job, err := clusterKFClient.WaitJob(jobUUID); err != nil { + return err + } else if job.Status != modules.JobStatusSuccess { + operationLog.Warn().Msgf("deleting job not succeeded, status: %v, job info: %v", job.Status, job) + } + } + } + + if cluster.ExtraAttribute.UseRegistrySecret { + if err := clusterEndpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(cluster.Namespace). + Delete(context.TODO(), imagePullSecretsNameFATE, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf(errors.Wrap(err, "error deleting registry secret").Error()) + } else { + operationLog.Info().Msgf("deleted registry secret %s", imagePullSecretsNameFATE) + } + } + + if cluster.CertConfig.PulsarServerCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := clusterEndpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(cluster.Namespace). + Delete(context.TODO(), entity.ParticipantFATESecretNamePulsar, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error deleting stale cert secret").Error()) + } else { + operationLog.Info().Msgf("deleted stale cert secret") + } + } + + if cluster.CertConfig.SitePortalServerCertInfo.BindingMode != entity.CertBindingModeSkip && cluster.CertConfig.SitePortalClientCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := clusterEndpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(cluster.Namespace). + Delete(context.TODO(), entity.ParticipantFATESecretNamePortal, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf(errors.Wrapf(err, "error deleting stale %s secret", entity.ParticipantFATESecretNamePortal).Error()) + } else { + operationLog.Info().Msgf("deleted stale %s secret", entity.ParticipantFATESecretNamePortal) + } + } + if cluster.ExtraAttribute.IsNewNamespace { + if err := clusterEndpointMgr.K8sClient().GetClientSet().CoreV1().Namespaces().Delete(context.TODO(), cluster.Namespace, v1.DeleteOptions{}); err != nil && !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete namespace") + } + operationLog.Info().Msgf("namespace %s deleted", cluster.Namespace) + } + } + return nil + }() + if err != nil { + operationLog.Error().Msgf(errors.Wrapf(err, "error uninstalling cluster").Error()) + if !force { + return + } + } + if deleteErr := s.ParticipantFATERepo.DeleteByUUID(cluster.UUID); deleteErr != nil { + operationLog.Error().Msgf(errors.Wrapf(deleteErr, "error deleting cluster from repo").Error()) + return + } + operationLog.Info().Msgf("uninstalled FATE cluster %s with UUID %s", cluster.Name, cluster.UUID) + }() + return wg, nil +} + +//CreateExternalCluster creates an external FATE cluster with the access info provided by user +func (s *ParticipantFATEService) CreateExternalCluster(req *ParticipantFATEExternalClusterCreationRequest) (*entity.ParticipantFATE, *sync.WaitGroup, error) { + if err := s.CheckPartyIDConflict(req.FederationUUID, req.PartyID); err != nil { + return nil, nil, err + } + federationUUID := req.FederationUUID + instance, err := s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationFATE) + + instance, err = s.ParticipantFATERepo.GetExchangeByFederationUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to check exchange existence status") + } + exchange := instance.(*entity.ParticipantFATE) + + if exchange.Status != entity.ParticipantFATEStatusActive { + return nil, nil, errors.Errorf("exchange %v is not in active status", exchange.UUID) + } + if exchange.IsManaged { + if err := s.EndpointService.TestKubeFATE(exchange.EndpointUUID); err != nil { + return nil, nil, err + } + } + cluster := &entity.ParticipantFATE{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.FederationUUID, + EndpointUUID: "Unknown", + ChartUUID: "Unknown", + Namespace: "Unknown", + ClusterUUID: "Unknown", + DeploymentYAML: "Unknown", + IsManaged: false, + }, + Type: entity.ParticipantFATETypeCluster, + PartyID: req.PartyID, + Status: entity.ParticipantFATEStatusActive, + AccessInfo: entity.ParticipantFATEModulesAccessMap{}, + } + cluster.AccessInfo[entity.ParticipantFATEServiceNamePulsar] = entity.ParticipantModulesAccess{ + ServiceType: corev1.ServiceType(entity.ParticipantDefaultServiceTypeNodePort.String()), + Host: req.PulsarAccessInfo.Host, + Port: req.PulsarAccessInfo.Port, + TLS: true, + FQDN: fmt.Sprintf("%d.%s", req.PartyID, federation.Domain), + } + cluster.AccessInfo[entity.ParticipantFATEServiceNameNginx] = entity.ParticipantModulesAccess{ + ServiceType: corev1.ServiceType(entity.ParticipantDefaultServiceTypeNodePort.String()), + Host: req.NginxAccessInfo.Host, + Port: req.NginxAccessInfo.Port, + TLS: false, + FQDN: "", + } + err = s.ParticipantFATERepo.Create(cluster) + if err != nil { + return nil, nil, err + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "created an external cluster", entity.EventLogLevelInfo) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + if exchange.IsManaged { + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "configuring external fate cluster").Str("uuid", cluster.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, message, eventLvl) + })) + if err := s.rebuildRouteTable(exchange); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "error rebuilding route table while creating cluster").Error()) + cluster.Status = entity.ParticipantFATEStatusFailed + if err := s.ParticipantFATERepo.UpdateStatusByUUID(cluster); err != nil { + operationLog.Error().Msg(errors.Wrap(err, "failed to update cluster status").Error()) + } + } + } + }() + return cluster, wg, nil +} + +func (s *ParticipantFATEService) buildNginxRouteTable(routeTable map[string]interface{}, cluster *entity.ParticipantFATE) { + routeTable[fmt.Sprintf("%d", cluster.PartyID)] = map[string][]nginxRouteTableEntry{ + "fateflow": { + { + Host: cluster.AccessInfo[entity.ParticipantFATEServiceNameNginx].Host, + HttpPort: cluster.AccessInfo[entity.ParticipantFATEServiceNameNginx].Port, + }, + }, + } +} + +func (s *ParticipantFATEService) buildATSRouteTable(routeTable map[string]interface{}, cluster *entity.ParticipantFATE) { + sniTable := routeTable["sni"] + if sniTable == nil { + sniTable = []interface{}{} + } + sniTable = append(sniTable.([]interface{}), atsRouteTableEntry{ + FQDN: cluster.AccessInfo[entity.ParticipantFATEServiceNamePulsar].FQDN, + TunnelRoute: fmt.Sprintf("%s:%d", cluster.AccessInfo[entity.ParticipantFATEServiceNamePulsar].Host, cluster.AccessInfo[entity.ParticipantFATEServiceNamePulsar].Port), + }) + routeTable["sni"] = sniTable +} + +func (s *ParticipantFATEService) rebuildRouteTable(exchange *entity.ParticipantFATE) error { + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "rebuilding fate route table").Str("federation_uuid", exchange.FederationUUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, message, eventLvl) + })) + operationLog.Info().Msg("start rebuilding route table") + instanceList, err := s.ParticipantFATERepo.ListByFederationUUID(exchange.FederationUUID) + if err != nil { + return errors.Wrap(err, "failed to list participants") + } + participantList := instanceList.([]entity.ParticipantFATE) + + exchangeEndpointMgr, exchangeKFClient, exchangeKFClientCloser, err := s.buildKubeFATEMgrAndClient(exchange.EndpointUUID) + if exchangeKFClientCloser != nil { + defer exchangeKFClientCloser() + } + if err != nil { + return errors.Errorf("cannot get exchange endpoint manager") + } + + var m map[string]interface{} + _ = yaml.Unmarshal([]byte(exchange.DeploymentYAML), &m) + + // reset the route table + m["nginx"].(map[string]interface{})["route_table"] = map[string]interface{}{} + m["trafficServer"].(map[string]interface{})["route_table"].(map[string]interface{})["sni"] = []interface{}{} + + for _, participant := range participantList { + if participant.Type == entity.ParticipantFATETypeCluster && participant.Status == entity.ParticipantFATEStatusActive { + s.buildNginxRouteTable(m["nginx"].(map[string]interface{})["route_table"].(map[string]interface{}), &participant) + s.buildATSRouteTable(m["trafficServer"].(map[string]interface{})["route_table"].(map[string]interface{}), &participant) + } + } + + updatedYaml, _ := yaml.Marshal(m) + exchange.DeploymentYAML = string(updatedYaml) + + if err := s.ParticipantFATERepo.UpdateDeploymentYAMLByUUID(exchange); err != nil { + return errors.Wrapf(err, "failed to update exchange info") + } + + retry := 5 + for { + if err := func() error { + operationLog.Info().Msgf("perform exchange deployment update with %v retries", retry) + jobUUID, err := exchangeKFClient.SubmitClusterUpdateJob(exchange.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "failed to submit exchange update job") + } + exchange.JobUUID = jobUUID + if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil { + return errors.Wrap(err, "failed to update exchange's job uuid") + } + operationLog.Info().Msgf("KubeFATE update job uuid: %s", jobUUID) + if job, err := exchangeKFClient.WaitJob(jobUUID); err != nil { + return errors.Wrapf(err, "failed to query exchange update job status") + } else if job.Status != modules.JobStatusSuccess { + return errors.Errorf("job is %s, job info: %v", job.Status.String(), job) + } + if err := deletePodWithPrefix(exchangeEndpointMgr.K8sClient(), exchange.Namespace, "traffic-server"); err != nil { + return err + } + return nil + }(); err != nil { + retry-- + operationLog.Error().Msgf(errors.Wrapf(err, "error updating exchange deployment, %v retries remaining", retry).Error()) + if retry == 0 { + return err + } + } else { + operationLog.Info().Msg("done updating exchange route table") + return nil + } + } +} + +func (s *ParticipantFATEService) loadParticipant(uuid string) (*entity.ParticipantFATE, error) { + instance, err := s.ParticipantFATERepo.GetByUUID(uuid) + if err != nil { + return nil, errors.Wrapf(err, "failed to query participant") + } + return instance.(*entity.ParticipantFATE), err +} + +func (s *ParticipantFATEService) BuildIngressInfoMap(participant *entity.ParticipantFATE) error { + var m map[string]interface{} + _ = yaml.Unmarshal([]byte(participant.DeploymentYAML), &m) + + i, ok := m["ingress"] + if !ok { + return nil + } + ingressNameList := i.(map[string]interface{}) + + ingressMap := entity.ParticipantFATEIngressMap{} + + mgr, _, closer, err := s.buildKubeFATEMgrAndClient(participant.EndpointUUID) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + + for name := range ingressNameList { + info, err := getIngressInfo(mgr.K8sClient(), name, participant.Namespace) + if err != nil { + return err + } + ingressMap[name] = *info + } + participant.IngressInfo = ingressMap + return nil +} + +// For mocking purpose +var ( + createATSSecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, serverCert *entity.Certificate, privateKey *rsa.PrivateKey) error { + certBytes, err := serverCert.EncodePEM() + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: entity.ParticipantFATESecretNameATS, + }, + Data: map[string][]byte{ + "proxy.key.pem": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }), + "proxy.cert.pem": certBytes, + "ca.cert.pem": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } + + createPulsarSecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, serverCert *entity.Certificate, privateKey *rsa.PrivateKey) error { + pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return err + } + + certBytes, err := serverCert.EncodePEM() + if err != nil { + return err + } + + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: entity.ParticipantFATESecretNamePulsar, + }, + Data: map[string][]byte{ + "broker.key-pk8.pem": pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Headers: nil, + Bytes: pkcs8Bytes, + }), + "broker.cert.pem": certBytes, + "ca.cert.pem": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } + + createTLSSecret = func(client kubernetes.Client, namespace string, serverCert *entity.Certificate, serverPrivateKey *rsa.PrivateKey, clientCert *entity.Certificate, clientPrivateKey *rsa.PrivateKey, caCert *x509.Certificate, secretName string) error { + serverCertBytes, err := serverCert.EncodePEM() + if err != nil { + return err + } + clientCertBytes, err := clientCert.EncodePEM() + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + }, + Data: map[string][]byte{ + "server.key": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(serverPrivateKey), + }), + "server.crt": serverCertBytes, + "client.key": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(clientPrivateKey), + }), + "client.crt": clientCertBytes, + "ca.crt": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } +) + +func getPulsarDomainFromYAML(yamlStr string) (string, error) { + type Exchange struct { + Domain string `json:"domain"` + } + type Pulsar struct { + Exchange Exchange `json:"exchange"` + } + type Cluster struct { + Pulsar Pulsar `json:"pulsar"` + } + var clusterDef Cluster + if err := yaml.Unmarshal([]byte(yamlStr), &clusterDef); err != nil { + return "", errors.Wrapf(err, "failed to extract pulsar exchange info") + } + return clusterDef.Pulsar.Exchange.Domain, nil +} diff --git a/server/domain/service/participant_fate_service_test.go b/server/domain/service/participant_fate_service_test.go new file mode 100644 index 00000000..4d801202 --- /dev/null +++ b/server/domain/service/participant_fate_service_test.go @@ -0,0 +1,169 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "crypto/rsa" + "crypto/x509" + "testing" + + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo/mock" + "github.com/FederatedAI/FedLCM/server/infrastructure/gorm" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgo "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + clientgotesting "k8s.io/client-go/testing" +) + +var getServiceAccessWithFallbackOrig = getServiceAccessWithFallback + +func TestCreateExchange_PosWithNewCert(t *testing.T) { + // stub the util functions that will be called + ensureNSExisting = func(client kubernetes.Client, namespace string) (bool, error) { + return true, nil + } + getServiceAccessWithFallback = func(client kubernetes.Client, namespace, serviceName, portName string, lbFallbackToNodePort bool) (serviceType corev1.ServiceType, host string, port int, err error) { + serviceType = corev1.ServiceTypeLoadBalancer + host = "test-host" + port = 8080 + return + } + createATSSecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, serverCert *entity.Certificate, privateKey *rsa.PrivateKey) error { + return nil + } + createTLSSecret = func(client kubernetes.Client, namespace string, serverCert *entity.Certificate, serverPrivateKey *rsa.PrivateKey, clientCert *entity.Certificate, clientPrivateKey *rsa.PrivateKey, caCert *x509.Certificate, secretName string) error { + return nil + } + + // restore + defer func() { + getServiceAccessWithFallback = getServiceAccessWithFallbackOrig + }() + + service := ParticipantFATEService{ + ParticipantFATERepo: &mock.ParticipantFATERepoMock{}, + ParticipantService: ParticipantService{ + FederationRepo: &mock.FederationFATERepoMock{}, + ChartRepo: &gorm.ChartMockRepo{}, + EventService: &mockEventServiceInt{}, + CertificateService: &mockParticipantFATECertificateServiceInt{}, + EndpointService: &mockParticipantFATEEndpointServiceInt{}, + }, + } + + exchange, wg, err := service.CreateExchange(&ParticipantFATEExchangeCreationRequest{ + ParticipantFATEExchangeYAMLCreationRequest: ParticipantFATEExchangeYAMLCreationRequest{ + ChartUUID: "3ce13cb2-5543-4b01-a5e4-9e4c4baa5973", // from the chart test repo + Name: "test-exchange", + Namespace: "test-ns", + ServiceType: entity.ParticipantDefaultServiceTypeLoadBalancer, + }, + ParticipantDeploymentBaseInfo: ParticipantDeploymentBaseInfo{ + Description: "", + EndpointUUID: "", + DeploymentYAML: `chartName: fate-exchange +chartVersion: v1.6.1-fedlcm-v0.2.0 +fmlManagerServer: + image: federatedai/fml-manager-server + imageTag: v0.2.0 + type: NodePort +modules: +- trafficServer +- nginx +- postgres +- fmlManagerServer +name: test-exchange +namespace: fate-exchange-test1 +nginx: + route_table: null + type: NodePort +partyId: 0 +podSecurityPolicy: + enabled: true +trafficServer: + route_table: + sni: null + type: NodePort`, + }, + FederationUUID: "", + ProxyServerCertInfo: entity.ParticipantComponentCertInfo{ + BindingMode: entity.CertBindingModeCreate, + UUID: "", + CommonName: "", + }, + FMLManagerServerCertInfo: entity.ParticipantComponentCertInfo{ + BindingMode: entity.CertBindingModeCreate, + UUID: "", + CommonName: "", + }, + FMLManagerClientCertInfo: entity.ParticipantComponentCertInfo{ + BindingMode: entity.CertBindingModeCreate, + UUID: "", + CommonName: "", + }, + }) + assert.NoError(t, err, "positive test should return with no error") + wg.Wait() + assert.Equal(t, entity.ParticipantFATEStatusActive, exchange.Status, "exchange status should be active") + assert.Equal(t, 3, len(exchange.AccessInfo), "exchange with fml-manager should expose 3 services") +} + +func TestGetServiceAccessWithFallback_PosFallbackToNodePort(t *testing.T) { + mockClient := &mockK8sClient{ + GetClientSetFn: func() clientgo.Interface { + fakeClientSet := &fake.Clientset{} + fakeClientSet.AddReactor("get", "services", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { + return true, &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "test-port", + Protocol: corev1.ProtocolTCP, + Port: 80, + NodePort: 30080, + }, + }, + ClusterIP: "10.0.0.1", + Type: corev1.ServiceTypeLoadBalancer, + }, + }, nil + }) + fakeClientSet.AddReactor("list", "nodes", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) { + return true, &corev1.NodeList{ + Items: []corev1.Node{ + { + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeExternalIP, + Address: "127.0.0.1", + }, + }, + }, + }, + }, + }, nil + }) + return fakeClientSet + }, + } + svcType, _, _, err := getServiceAccessWithFallbackOrig(mockClient, "test-ns", "test-svc", "test-port", true) + assert.Equal(t, corev1.ServiceTypeNodePort, svcType, "should be a NodePort service") + assert.NoError(t, err, "positive test should return no error") +} diff --git a/server/domain/service/participant_openfl_service.go b/server/domain/service/participant_openfl_service.go new file mode 100644 index 00000000..fbec4b67 --- /dev/null +++ b/server/domain/service/participant_openfl_service.go @@ -0,0 +1,1205 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "github.com/rs/zerolog/log" + "math/rand" + "net/url" + "os" + "strings" + "sync" + "text/template" + + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/server/infrastructure/gorm/mock" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/modules" + "github.com/Masterminds/sprig/v3" + "github.com/pkg/errors" + "github.com/rs/zerolog" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +const ( + imagePullSecretsNameOpenFL = "registrykeyopenfl" +) + +// ParticipantOpenFLService is the service to manage openfl participants +type ParticipantOpenFLService struct { + ParticipantOpenFLRepo repo.ParticipantOpenFLRepository + TokenRepo repo.RegistrationTokenRepository + InfraRepo repo.InfraProviderRepository + ParticipantService +} + +// ParticipantOpenFLDirectorYAMLCreationRequest contains necessary info to generate the deployment yaml +// content for KubeFATE to deploy OpenFL director +type ParticipantOpenFLDirectorYAMLCreationRequest struct { + FederationUUID string `json:"federation_uuid" form:"federation_uuid"` + ChartUUID string `json:"chart_uuid" form:"chart_uuid"` + Name string `json:"name" form:"name"` + Namespace string `json:"namespace" form:"namespace"` + ServiceType entity.ParticipantDefaultServiceType `json:"service_type" form:"service_type"` + // for generating the yaml, RegistrySecretConfig is not used in RegistryConfig + RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config"` + JupyterPassword string `json:"jupyter_password" form:"jupyter_password"` + EnablePSP bool `json:"enable_psp" form:"enable_psp"` +} + +// ParticipantOpenFLDirectorCreationRequest is the director creation request +type ParticipantOpenFLDirectorCreationRequest struct { + ParticipantOpenFLDirectorYAMLCreationRequest + ParticipantDeploymentBaseInfo + DirectorServerCertInfo entity.ParticipantComponentCertInfo `json:"director_server_cert_info"` + JupyterClientCertInfo entity.ParticipantComponentCertInfo `json:"jupyter_client_cert_info"` +} + +// ParticipantOpenFLEnvoyRegistrationRequest is the registration request from an envoy +type ParticipantOpenFLEnvoyRegistrationRequest struct { + // required + KubeConfig string `json:"kubeconfig"` + TokenStr string `json:"token"` + + // optional + Name string `json:"name"` + Description string `json:"description"` + Namespace string `json:"namespace"` + ChartUUID string `json:"chart_uuid"` + Labels valueobject.Labels `json:"labels"` + ConfigYAML string `json:"config_yaml"` + SkipCommonPythonFiles bool `json:"skip_common_python_files"` + RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config"` + EnablePSP bool `json:"enable_psp"` + + // internal + federation *entity.FederationOpenFL + caCert *x509.Certificate + operationLog *zerolog.Logger + chart *entity.Chart +} + +// GetOpenFLDirectorYAML returns the exchange deployment yaml content +func (s *ParticipantOpenFLService) GetOpenFLDirectorYAML(req *ParticipantOpenFLDirectorYAMLCreationRequest) (string, error) { + instance, err := s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return "", errors.Wrapf(err, "failed to query chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeOpenFLDirector { + return "", errors.Errorf("chart %s is not for OpenFL director deployment", chart.UUID) + } + + t, err := template.New("openfl-director").Parse(chart.InitialYamlTemplate) + if err != nil { + return "", err + } + + data := struct { + Name string + Namespace string + JupyterPassword string + ServiceType string + UseRegistry bool + Registry string + UseImagePullSecrets bool + ImagePullSecretsName string + SampleShape string + TargetShape string + EnablePSP bool + Domain string + }{ + Name: toDeploymentName(req.Name), + Namespace: req.Namespace, + JupyterPassword: req.JupyterPassword, + ServiceType: req.ServiceType.String(), + UseRegistry: req.RegistryConfig.UseRegistry, + Registry: req.RegistryConfig.Registry, + UseImagePullSecrets: req.RegistryConfig.UseRegistrySecret, + ImagePullSecretsName: imagePullSecretsNameOpenFL, + EnablePSP: req.EnablePSP, + } + + federationUUID := req.FederationUUID + instance, err = s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return "", errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationOpenFL) + data.Domain = federation.Domain + if federation.UseCustomizedShardDescriptor { + var arr []string + for _, item := range federation.ShardDescriptorConfig.SampleShape { + arr = append(arr, `'`+item+`'`) + } + data.SampleShape = strings.Join(arr, ", ") + data.SampleShape = `[` + data.SampleShape + `]` + + arr = []string{} + for _, item := range federation.ShardDescriptorConfig.TargetShape { + arr = append(arr, `'`+item+`'`) + } + data.TargetShape = strings.Join(arr, ", ") + data.TargetShape = `[` + data.TargetShape + `]` + } else { + data.SampleShape = `['1']` + data.TargetShape = `['1']` + } + + if data.JupyterPassword != "" { + b := "" + for i := 0; i < 8; i++ { + b += fmt.Sprintf("%v", rand.Intn(10)) + } + checkSum := sha1.Sum([]byte(data.JupyterPassword + b)) + // jupyter's password mechanism - must contain a salt + data.JupyterPassword = "sha1:" + b + ":" + hex.EncodeToString(checkSum[:]) + } + + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// CreateDirector creates an OpenFL director +func (s *ParticipantOpenFLService) CreateDirector(req *ParticipantOpenFLDirectorCreationRequest) (*entity.ParticipantOpenFL, *sync.WaitGroup, error) { + federationUUID := req.FederationUUID + instance, err := s.FederationRepo.GetByUUID(federationUUID) + if err != nil { + return nil, nil, errors.Wrap(err, "error getting federation info") + } + federation := instance.(*entity.FederationOpenFL) + + if exist, err := s.ParticipantOpenFLRepo.IsDirectorCreatedByFederationUUID(federationUUID); err != nil { + return nil, nil, errors.Wrapf(err, "failed to check director existence status") + } else if exist { + return nil, nil, errors.Errorf("a director is already deployed in federation %s", federationUUID) + } + + if req.DirectorServerCertInfo.BindingMode == entity.CertBindingModeReuse || + req.JupyterClientCertInfo.BindingMode == entity.CertBindingModeReuse { + return nil, nil, errors.New("cannot re-use existing certificate") + } + + if err := s.EndpointService.TestKubeFATE(req.EndpointUUID); err != nil { + return nil, nil, err + } + + instance, err = s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return nil, nil, errors.Wrapf(err, "faile to get chart") + } + chart := instance.(*entity.Chart) + if chart.Type != entity.ChartTypeOpenFLDirector { + return nil, nil, errors.Errorf("chart %s is not for OpenFL director deployment", chart.UUID) + } + + var m map[string]interface{} + err = yaml.Unmarshal([]byte(req.DeploymentYAML), &m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to unmarshal deployment yaml") + } + + m["name"] = toDeploymentName(req.Name) + m["namespace"] = req.Namespace + + finalYAMLBytes, err := yaml.Marshal(m) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get final yaml content") + } + req.DeploymentYAML = string(finalYAMLBytes) + log.Debug().Msgf("openfl director deployment yaml: %s", req.DeploymentYAML) + + var caCert *x509.Certificate + if req.DirectorServerCertInfo.BindingMode == entity.CertBindingModeCreate || + req.JupyterClientCertInfo.BindingMode == entity.CertBindingModeCreate { + log.Info().Msg("preparing CA for issuing certificate for openfl director deployment") + ca, err := s.CertificateService.DefaultCA() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get default CA") + } + caCert, err = ca.RootCert() + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get CA cert") + } + } + + director := &entity.ParticipantOpenFL{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.FederationUUID, + EndpointUUID: req.EndpointUUID, + ChartUUID: req.ChartUUID, + Namespace: req.Namespace, + DeploymentYAML: req.DeploymentYAML, + IsManaged: true, + ExtraAttribute: entity.ParticipantExtraAttribute{ + IsNewNamespace: false, + UseRegistrySecret: req.RegistryConfig.UseRegistrySecret, + }, + }, + Type: entity.ParticipantOpenFLTypeDirector, + Status: entity.ParticipantOpenFLStatusInstallingDirector, + CertConfig: entity.ParticipantOpenFLCertConfig{ + DirectorServerCertInfo: req.DirectorServerCertInfo, + JupyterClientCertInfo: req.JupyterClientCertInfo, + }, + AccessInfo: entity.ParticipantOpenFLModulesAccessMap{}, + } + err = s.ParticipantOpenFLRepo.Create(director) + if err != nil { + return nil, nil, err + } + + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLDirector, director.UUID, "start creating director", entity.EventLogLevelInfo) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "installing openfl director").Str("uuid", director.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLDirector, director.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("creating openfl director %s with UUID %s", req.Name, director.UUID) + if err := func() error { + endpointMgr, kfClient, clientCloser, err := s.buildKubeFATEMgrAndClient(req.EndpointUUID) + if clientCloser != nil { + defer clientCloser() + } + if err != nil { + return err + } + if chart.Private { + if err := kfClient.EnsureChartExist(chart.ChartName, chart.Version, chart.ArchiveContent); err != nil { + return errors.Wrapf(err, "error uploading FedLCM private chart") + } + } + + if director.ExtraAttribute.IsNewNamespace, err = ensureNSExisting(endpointMgr.K8sClient(), req.Namespace); err != nil { + return err + } + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update exchange attribute") + } + + //Create registry secret + if req.RegistryConfig.UseRegistrySecret { + if err := createRegistrySecret(endpointMgr.K8sClient(), imagePullSecretsNameOpenFL, req.Namespace, req.RegistryConfig.RegistrySecretConfig); err != nil { + return errors.Wrap(err, "failed to create registry secret") + } else { + operationLog.Info().Msgf("created registry secret %s with username %s for URL %s", imagePullSecretsNameOpenFL, req.RegistryConfig.RegistrySecretConfig.Username, req.RegistryConfig.RegistrySecretConfig.ServerURL) + } + } + + directorFQDN := req.DirectorServerCertInfo.CommonName + if directorFQDN == "" { + directorFQDN = fmt.Sprintf("director.%s", federation.Domain) + } + if req.DirectorServerCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.DirectorServerCertInfo.CommonName == "" { + req.DirectorServerCertInfo.CommonName = directorFQDN + } + operationLog.Info().Msgf("creating certificate for the director server with CN: %s", directorFQDN) + cert, pk, err := s.CertificateService.CreateCertificateSimple(directorFQDN, defaultCertLifetime, []string{directorFQDN, "director"}) + if err != nil { + return errors.Wrapf(err, "failed to create director certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", cert.SerialNumber, cert.Subject.CommonName) + err = createDirectorSecret(endpointMgr.K8sClient(), req.Namespace, caCert, cert, pk) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(cert, entity.CertificateBindingServiceTypeOpenFLDirector, director.UUID, federationUUID, entity.FederationTypeOpenFL); err != nil { + return err + } + director.CertConfig.DirectorServerCertInfo.CommonName = req.DirectorServerCertInfo.CommonName + director.CertConfig.DirectorServerCertInfo.UUID = cert.UUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update director cert info") + } + } + + jupyterCN := req.JupyterClientCertInfo.CommonName + if jupyterCN == "" { + jupyterCN = fmt.Sprintf("jupyter.%s", federation.Domain) + } + if req.JupyterClientCertInfo.BindingMode == entity.CertBindingModeCreate { + if req.JupyterClientCertInfo.CommonName == "" { + req.JupyterClientCertInfo.CommonName = jupyterCN + } + operationLog.Info().Msgf("creating certificate for the jupyter client with CN: %s", req.JupyterClientCertInfo.CommonName) + cert, pk, err := s.CertificateService.CreateCertificateSimple(jupyterCN, defaultCertLifetime, []string{jupyterCN}) + if err != nil { + return errors.Wrapf(err, "failed to create jupyter certificate") + } + operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", cert.SerialNumber, cert.Subject.CommonName) + err = createJupyterSecret(endpointMgr.K8sClient(), req.Namespace, caCert, cert, pk) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(cert, entity.CertificateBindingServiceTypeOpenFLJupyter, director.UUID, federationUUID, entity.FederationTypeOpenFL); err != nil { + return err + } + director.CertConfig.JupyterClientCertInfo.CommonName = req.JupyterClientCertInfo.CommonName + director.CertConfig.JupyterClientCertInfo.UUID = cert.UUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update jupyter cert info") + } + } + + jobUUID, err := kfClient.SubmitClusterInstallationJob(director.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "fail to submit cluster creation request") + } + operationLog.Info().Msgf("director cluster installing job UUID: %s", jobUUID) + director.JobUUID = jobUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update director's job uuid") + } + clusterUUID, err := kfClient.WaitClusterUUID(jobUUID) + if err != nil { + return errors.Wrapf(err, "fail to get cluster uuid") + } + director.ClusterUUID = clusterUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update director cluster uuid") + } + //wait for job finished + job, err := kfClient.WaitJob(jobUUID) + if err != nil { + return err + } + if job.Status != modules.JobStatusSuccess { + return errors.Errorf("job is %s, job info: %v", job.Status.String(), job) + } + + serviceType, host, port, err := getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantOpenFLServiceNameDirector), "director") + if err != nil { + return errors.Wrapf(err, "fail to get director api access info") + } + director.AccessInfo[entity.ParticipantOpenFLServiceNameDirector] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: directorFQDN, + } + + serviceType, host, port, err = getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantOpenFLServiceNameDirector), "agg") + if err != nil { + return errors.Wrapf(err, "fail to get director agg access info") + } + director.AccessInfo[entity.ParticipantOpenFLServiceNameAggregator] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: true, + FQDN: directorFQDN, + } + + serviceType, host, port, err = getServiceAccess(endpointMgr.K8sClient(), req.Namespace, string(entity.ParticipantOpenFLServiceNameJupyter), "notebook") + if err != nil { + return errors.Wrapf(err, "fail to get jupyter access info") + } + director.AccessInfo[entity.ParticipantOpenFLServiceNameJupyter] = entity.ParticipantModulesAccess{ + ServiceType: serviceType, + Host: host, + Port: port, + TLS: false, + } + + director.Status = entity.ParticipantOpenFLStatusActive + return s.ParticipantOpenFLRepo.UpdateInfoByUUID(director) + }(); err != nil { + operationLog.Error().Msgf("failed to install openfl director, error: %v", err) + director.Status = entity.ParticipantOpenFLStatusFailed + if updateErr := s.ParticipantOpenFLRepo.UpdateStatusByUUID(director); updateErr != nil { + operationLog.Error().Msgf("failed to update openfl director status, error: %v", updateErr) + } + return + } + operationLog.Info().Msgf("openfl director %s(%s) deployed", director.Name, director.UUID) + }() + + return director, wg, nil +} + +// RemoveDirector removes and uninstalls an OpenFL director +func (s *ParticipantOpenFLService) RemoveDirector(uuid string, force bool) (*sync.WaitGroup, error) { + director, err := s.loadParticipant(uuid) + if err != nil { + return nil, err + } + if director.Type != entity.ParticipantOpenFLTypeDirector { + return nil, errors.Errorf("participant %s is not an OpenFL director", director.UUID) + } + + if !force && director.Status != entity.ParticipantOpenFLStatusActive { + return nil, errors.Errorf("director cannot be removed when in status: %v", director.Status) + } + + instanceList, err := s.ParticipantOpenFLRepo.ListByFederationUUID(director.FederationUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to list participants in federation") + } + participantList := instanceList.([]entity.ParticipantOpenFL) + if len(participantList) > 1 { + return nil, errors.Errorf("cannot remove director as there are %v envoy(s) in this federation", len(participantList)-1) + } + + director.Status = entity.ParticipantOpenFLStatusRemoving + if err := s.ParticipantOpenFLRepo.UpdateStatusByUUID(director); err != nil { + return nil, errors.Wrapf(err, "failed to update director status") + } + + // TODO: revoke the certificate, after we have some OCSP mechanism in place + if err := s.CertificateService.RemoveBinding(director.UUID); err != nil { + return nil, errors.Wrapf(err, "failed to remove certificate bindings") + } + + //record removing event + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLDirector, director.UUID, "start removing director", entity.EventLogLevelInfo) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "uninstalling openfl director").Str("uuid", director.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLDirector, director.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("uninstalling OpenFL director %s with UUID %s", director.Name, director.UUID) + err := func() error { + endpointMgr, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(director.EndpointUUID) + if kfClientCloser != nil { + defer kfClientCloser() + } + if err != nil { + return err + } + if director.JobUUID != "" { + operationLog.Info().Msgf("try to stop KubeFATE job with UUID %s", director.JobUUID) + err := kfClient.StopJob(director.JobUUID) + if err != nil { + return err + } + } + if director.ClusterUUID != "" { + operationLog.Info().Msgf("delete KubeFATE cluster with UUID %s", director.ClusterUUID) + jobUUID, err := kfClient.SubmitClusterDeletionJob(director.ClusterUUID) + if err != nil { + // TODO: use helm or client-go to try to further clean things up + return err + } + if jobUUID != "" { + director.JobUUID = jobUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(director); err != nil { + return errors.Wrap(err, "failed to update director's job uuid") + } + if _, err := kfClient.WaitJob(jobUUID); err != nil { + return err + } + } + } + // delete registry secret + if director.ExtraAttribute.UseRegistrySecret { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(director.Namespace). + Delete(context.TODO(), imagePullSecretsNameOpenFL, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf("error deleting registry secret: %v", err) + } else { + operationLog.Info().Msgf("deleted registry secret %s", imagePullSecretsNameOpenFL) + } + } + + // delete certs secrets + if director.CertConfig.DirectorServerCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(director.Namespace). + Delete(context.TODO(), entity.ParticipantOpenFLSecretNameDirector, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf("error deleting stale director cert secret: %v", err) + } else { + operationLog.Info().Msgf("deleted stale director cert secret") + } + } + if director.CertConfig.JupyterClientCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(director.Namespace). + Delete(context.TODO(), entity.ParticipantOpenFLSecretNameJupyter, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf("error deleting stale %s secret: %v", entity.ParticipantOpenFLSecretNameJupyter, err) + } else { + operationLog.Info().Msgf("deleted stale %s secret", entity.ParticipantOpenFLSecretNameJupyter) + } + } + // finally, delete the namespace + if director.ExtraAttribute.IsNewNamespace { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Namespaces().Delete(context.TODO(), director.Namespace, v1.DeleteOptions{}); err != nil && !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete namespace") + } + operationLog.Info().Msgf("namespace %s deleted", director.Namespace) + } + return nil + }() + if err != nil { + operationLog.Error().Msgf("error uninstalling openfl director, error: %v", err) + if !force { + return + } + } + if deleteErr := s.ParticipantOpenFLRepo.DeleteByUUID(director.UUID); deleteErr != nil { + operationLog.Error().Msgf("error deleting director from repo, error: %v", err) + return + } + operationLog.Info().Msgf("uninstalled OpenFL director %s with UUID %s", director.Name, director.UUID) + }() + return wg, nil +} + +// HandleRegistrationRequest process a Envoy device registration request +func (s *ParticipantOpenFLService) HandleRegistrationRequest(req *ParticipantOpenFLEnvoyRegistrationRequest) (*entity.ParticipantOpenFL, error) { + token, err := s.validateEnvoyToken(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to validate token") + } + + instance, err := s.FederationRepo.GetByUUID(token.FederationUUID) + if err != nil { + return nil, err + } + req.federation = instance.(*entity.FederationOpenFL) + + var caCert *x509.Certificate + ca, err := s.CertificateService.DefaultCA() + if err != nil { + return nil, errors.Wrapf(err, "failed to get default CA") + } + caCert, err = ca.RootCert() + if err != nil { + return nil, errors.Wrapf(err, "failed to get CA cert") + } + req.caCert = caCert + + if req.ChartUUID == "" { + req.ChartUUID = "c62b27a6-bf0f-4515-840a-2554ed63aa56" + } + instance, err = s.ChartRepo.GetByUUID(req.ChartUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to query chart") + } + req.chart = instance.(*entity.Chart) + if req.chart.Type != entity.ChartTypeOpenFLEnvoy { + return nil, errors.Errorf("chart %s is not for OpenFL envoy deployment", req.chart.UUID) + } + + infraProvider, err := s.configEnvoyInfra(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to prepare the envoy infra provider") + } + + if req.Name == "" { + infraAPIHost, err := infraProvider.Config.APIHost() + if err != nil { + return nil, err + } + + u, err := url.Parse(infraAPIHost) + if err != nil { + return nil, err + } + req.Name = fmt.Sprintf("envoy-%s", toDeploymentName(u.Hostname())) + } + //TODO: check if same name envoy exists in the same infra + + if req.Namespace == "" { + req.Namespace = fmt.Sprintf("%s-envoy", toDeploymentName(req.federation.Name)) + } + K8sClient, err := kubernetes.NewKubernetesClient("", infraProvider.Config.KubeConfigContent) + if err != nil { + return nil, err + } + _, err = K8sClient.GetClientSet().CoreV1().Namespaces().Get(context.TODO(), req.Namespace, v1.GetOptions{}) + if err == nil { + return nil, errors.Errorf("namespace %s exists. cannot override", req.Namespace) + } + + deploymentYAML, err := s.GetOpenFLEnvoyYAML(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to get deployment yaml") + } + + envoy := &entity.ParticipantOpenFL{ + Participant: entity.Participant{ + UUID: uuid.NewV4().String(), + Name: req.Name, + Description: req.Description, + FederationUUID: req.federation.UUID, + EndpointUUID: "", + ChartUUID: req.chart.UUID, + Namespace: req.Namespace, + ClusterUUID: "", + JobUUID: "", + DeploymentYAML: deploymentYAML, + IsManaged: true, + ExtraAttribute: entity.ParticipantExtraAttribute{ + IsNewNamespace: false, + UseRegistrySecret: false, + }, + }, + Type: entity.ParticipantOpenFLTypeEnvoy, + Status: entity.ParticipantOpenFLStatusInstallingEndpoint, + InfraUUID: infraProvider.UUID, + TokenUUID: token.UUID, + CertConfig: entity.ParticipantOpenFLCertConfig{ + EnvoyClientCertInfo: entity.ParticipantComponentCertInfo{ + BindingMode: entity.CertBindingModeCreate, + }, + }, + AccessInfo: nil, + Labels: valueobject.Labels{}, + } + + for k, v := range token.Labels { + envoy.Labels[k] = v + } + for k, v := range req.Labels { + envoy.Labels[k] = v + } + + if err := s.ParticipantOpenFLRepo.Create(envoy); err != nil { + return nil, err + } + + go func() { + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "installing envoy").Str("uuid", envoy.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLEnvoy, envoy.UUID, message, eventLvl) + })) + req.operationLog = &operationLog + operationLog.Info().Msgf("creating envoy %s with UUID %s", req.Name, envoy.UUID) + if err := func() (err error) { + endpointUUID, err := s.EndpointService.ensureEndpointExist(infraProvider.UUID) + if err != nil { + return err + } + envoy.Status = entity.ParticipantOpenFLStatusInstallingEnvoy + envoy.EndpointUUID = endpointUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return err + } + operationLog.Info().Msgf("kubefate endpoint prepared") + return s.installEnvoyInstance(req, envoy) + }(); err != nil { + operationLog.Error().Msgf("failed to install openfl envoy, error: %v", err) + envoy.Status = entity.ParticipantOpenFLStatusFailed + if updateErr := s.ParticipantOpenFLRepo.UpdateStatusByUUID(envoy); updateErr != nil { + operationLog.Error().Msgf("failed to update envoy status, error: %v", updateErr) + } + } + }() + return envoy, nil +} + +// GetOpenFLEnvoyYAML generates the envoy deployment yaml based on the envoy registration request +func (s *ParticipantOpenFLService) GetOpenFLEnvoyYAML(req *ParticipantOpenFLEnvoyRegistrationRequest) (string, error) { + chart := req.chart + federation := req.federation + + instance, err := s.ParticipantOpenFLRepo.GetDirectorByFederationUUID(federation.UUID) + if err != nil { + return "", errors.Wrapf(err, "failed to check director existence status") + } + director := instance.(*entity.ParticipantOpenFL) + + if director.Status != entity.ParticipantOpenFLStatusActive { + return "", errors.Errorf("director %v is not in active status", director.UUID) + } + + accessInfoMap := director.AccessInfo + if accessInfoMap == nil { + return "", errors.New("director access info is missing") + } + data := struct { + Name string + Namespace string + DirectorFQDN string + DirectorIP string + DirectorPort int + AggPort int + Domain string + UseRegistry bool + Registry string + UseImagePullSecrets bool + ImagePullSecretsName string + EnvoyConfig string + EnablePSP bool + }{ + Name: toDeploymentName(req.Name), + Namespace: req.Namespace, + Domain: federation.Domain, + EnvoyConfig: mock.DefaultEnvoyConfig, + UseRegistry: req.RegistryConfig.UseRegistry, + Registry: req.RegistryConfig.Registry, + UseImagePullSecrets: req.RegistryConfig.UseRegistrySecret, + ImagePullSecretsName: imagePullSecretsNameOpenFL, + EnablePSP: req.EnablePSP, + } + if directorAccess, ok := accessInfoMap[entity.ParticipantOpenFLServiceNameDirector]; !ok { + return "", errors.New("missing director access info") + } else { + data.DirectorFQDN = directorAccess.FQDN + data.DirectorIP = directorAccess.Host + data.DirectorPort = directorAccess.Port + } + if aggAccess, ok := accessInfoMap[entity.ParticipantOpenFLServiceNameAggregator]; !ok { + return "", errors.New("missing director agg access info") + } else { + data.AggPort = aggAccess.Port + } + + if req.ConfigYAML != "" { + data.EnvoyConfig = req.ConfigYAML + } else if federation.UseCustomizedShardDescriptor { + data.EnvoyConfig = federation.ShardDescriptorConfig.EnvoyConfigYaml + } + + t, err := template.New("openfl-envoy").Funcs(sprig.TxtFuncMap()).Parse(chart.InitialYamlTemplate) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// RemoveEnvoy removes and uninstalls an OpenFL envoy +func (s *ParticipantOpenFLService) RemoveEnvoy(uuid string, force bool) error { + envoy, err := s.loadParticipant(uuid) + if err != nil { + return err + } + if envoy.Type != entity.ParticipantOpenFLTypeEnvoy { + return errors.Errorf("participant %s is not an OpenFL envoy", envoy.UUID) + } + + if !force && envoy.Status != entity.ParticipantOpenFLStatusActive { + return errors.Errorf("director cannot be removed when in status: %v", envoy.Status) + } + + envoy.Status = entity.ParticipantOpenFLStatusRemoving + if err := s.ParticipantOpenFLRepo.UpdateStatusByUUID(envoy); err != nil { + return errors.Wrapf(err, "failed to update director status") + } + + // TODO: revoke the certificate + if err := s.CertificateService.RemoveBinding(envoy.UUID); err != nil { + return errors.Wrapf(err, "failed to remove certificate bindings") + } + + // record removing event + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLEnvoy, envoy.UUID, "start removing envoy", entity.EventLogLevelInfo) + + go func() { + operationLog := zerolog.New(os.Stderr).With().Timestamp().Str("action", "uninstalling openfl envoy").Str("uuid", envoy.UUID).Logger(). + Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + eventLvl := entity.EventLogLevelInfo + if level == zerolog.ErrorLevel { + eventLvl = entity.EventLogLevelError + } + _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeOpenFLEnvoy, envoy.UUID, message, eventLvl) + })) + operationLog.Info().Msgf("uninstalling OpenFL envoy %s with UUID %s", envoy.Name, envoy.UUID) + err := func() error { + endpointMgr, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(envoy.EndpointUUID) + if kfClientCloser != nil { + defer kfClientCloser() + } + if err != nil { + return err + } + if envoy.JobUUID != "" { + operationLog.Info().Msgf("try to stop envoy job: %s", envoy.JobUUID) + err := kfClient.StopJob(envoy.JobUUID) + if err != nil { + return err + } + } + if envoy.ClusterUUID != "" { + operationLog.Info().Msgf("try to delete envoy cluster in KubeFATE with uuid: %s", envoy.ClusterUUID) + jobUUID, err := kfClient.SubmitClusterDeletionJob(envoy.ClusterUUID) + if err != nil { + // TODO: use helm or client-go to try to clean things up + return err + } + if jobUUID != "" { + envoy.JobUUID = jobUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to update envoy's job uuid") + } + if _, err := kfClient.WaitJob(jobUUID); err != nil { + return err + } + } + } + //Delete registry secret + if envoy.ExtraAttribute.UseRegistrySecret { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(envoy.Namespace). + Delete(context.TODO(), imagePullSecretsNameOpenFL, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf("error deleting registry secret: %v", err) + } else { + operationLog.Info().Msgf("deleted registry secret %s", imagePullSecretsNameOpenFL) + } + } + if envoy.CertConfig.EnvoyClientCertInfo.BindingMode != entity.CertBindingModeSkip { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Secrets(envoy.Namespace). + Delete(context.TODO(), entity.ParticipantOpenFLSecretNameEnvoy, v1.DeleteOptions{}); err != nil { + operationLog.Error().Msgf("error deleting stale %s secret: %v", entity.ParticipantOpenFLSecretNameEnvoy, err) + } else { + operationLog.Info().Msgf("deleted stale %s secret", entity.ParticipantOpenFLSecretNameEnvoy) + } + } + if envoy.ExtraAttribute.IsNewNamespace { + if err := endpointMgr.K8sClient().GetClientSet().CoreV1().Namespaces().Delete(context.TODO(), envoy.Namespace, v1.DeleteOptions{}); err != nil && !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to delete namespace") + } + operationLog.Info().Msgf("namespace %s deleted", envoy.Namespace) + } + return nil + }() + if err != nil { + operationLog.Error().Msgf("error uninstalling openfl envoy: %v", err) + if !force { + return + } + } + if deleteErr := s.ParticipantOpenFLRepo.DeleteByUUID(envoy.UUID); deleteErr != nil { + operationLog.Info().Msgf("error deleting envoy from repo: %v", deleteErr) + return + } + operationLog.Info().Msgf("uninstalled OpenFL envoy %s with UUID %s", envoy.Name, envoy.UUID) + }() + return nil +} + +func (s *ParticipantOpenFLService) loadParticipant(uuid string) (*entity.ParticipantOpenFL, error) { + instance, err := s.ParticipantOpenFLRepo.GetByUUID(uuid) + if err != nil { + return nil, errors.Wrapf(err, "failed to query participant") + } + return instance.(*entity.ParticipantOpenFL), err +} + +func (s *ParticipantOpenFLService) configEnvoyInfra(req *ParticipantOpenFLEnvoyRegistrationRequest) (*entity.InfraProviderKubernetes, error) { + if req.federation == nil { + return nil, errors.New("missing federation") + } + kubeconfig := valueobject.KubeConfig{ + KubeConfigContent: req.KubeConfig, + } + if err := kubeconfig.Validate(); err != nil { + return nil, err + } + + infraAPIHost, err := kubeconfig.APIHost() + if err != nil { + return nil, err + } + + u, err := url.Parse(infraAPIHost) + if err != nil { + return nil, err + } + + var infraProvider *entity.InfraProviderKubernetes + infraProviderInstance, err := s.InfraRepo.GetByAddress(infraAPIHost) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + infraProvider = &entity.InfraProviderKubernetes{ + InfraProviderBase: entity.InfraProviderBase{ + Name: u.Hostname(), + Description: "added during registering OpenFL envoy", + Type: entity.InfraProviderTypeK8s, + }, + Config: kubeconfig, + Repo: s.InfraRepo, + } + log.Info().Msgf("creating infra provider during envoy registration, name: %s", infraProvider.Name) + if err := infraProvider.Create(); err != nil { + return nil, err + } + } else { + return nil, err + } + } else { + infraProvider = infraProviderInstance.(*entity.InfraProviderKubernetes) + } + return infraProvider, nil +} + +func (s *ParticipantOpenFLService) validateEnvoyToken(req *ParticipantOpenFLEnvoyRegistrationRequest) (*entity.RegistrationTokenOpenFL, error) { + tokenType, tokenStr, err := entity.RegistrationTokenParse(req.TokenStr) + if err != nil { + return nil, err + } + token := &entity.RegistrationTokenOpenFL{ + RegistrationToken: entity.RegistrationToken{ + TokenType: tokenType, + TokenStr: tokenStr, + Repo: s.TokenRepo, + }, + ParticipantRepo: s.ParticipantOpenFLRepo, + } + if err := s.TokenRepo.LoadByTypeAndStr(token); err != nil { + return nil, err + } + if err := token.Validate(); err != nil { + return nil, err + } + return token, nil +} + +func (s *ParticipantOpenFLService) installEnvoyInstance(req *ParticipantOpenFLEnvoyRegistrationRequest, envoy *entity.ParticipantOpenFL) error { + if envoy.EndpointUUID == "" { + return errors.New("missing endpoint uuid") + } + if req.federation == nil { + return errors.New("missing federation instance") + } + if req.caCert == nil { + return errors.New("missing ca cert") + } + if req.operationLog == nil { + return errors.New("missing logger") + } + if req.chart == nil { + return errors.New("missing chart") + } + endpointMgr, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(envoy.EndpointUUID) + if kfClientCloser != nil { + defer kfClientCloser() + } + if err != nil { + return err + } + if req.chart.Private { + if err := kfClient.EnsureChartExist(req.chart.ChartName, req.chart.Version, req.chart.ArchiveContent); err != nil { + return errors.Wrapf(err, "error uploading FedLCM private chart") + } + } + + if envoy.ExtraAttribute.IsNewNamespace, err = ensureNSExisting(endpointMgr.K8sClient(), req.Namespace); err != nil { + return err + } + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to update cluster attribute") + } + + if !req.SkipCommonPythonFiles { + req.operationLog.Info().Msgf("creating shard descriptor python files") + pythonFiles := mock.DefaultEnvoyPythonConfig + if req.federation.UseCustomizedShardDescriptor && len(req.federation.ShardDescriptorConfig.PythonFiles) > 0 { + pythonFiles = req.federation.ShardDescriptorConfig.PythonFiles + } + if err := createEnvoyShardDescriptor(endpointMgr.K8sClient(), req.Namespace, pythonFiles); err != nil { + return err + } + } + + if req.RegistryConfig.UseRegistrySecret { + if err := createRegistrySecret(endpointMgr.K8sClient(), imagePullSecretsNameOpenFL, req.Namespace, req.RegistryConfig.RegistrySecretConfig); err != nil { + return errors.Wrap(err, "failed to create registry secret") + } else { + req.operationLog.Info().Msgf("created registry secret %s with username %s for URL %s", imagePullSecretsNameOpenFL, req.RegistryConfig.RegistrySecretConfig.Username, req.RegistryConfig.RegistrySecretConfig.ServerURL) + } + } + + if envoy.CertConfig.EnvoyClientCertInfo.BindingMode == entity.CertBindingModeCreate { + envoy.CertConfig.EnvoyClientCertInfo.CommonName = req.Name + req.operationLog.Info().Msgf("creating certificate for the envoy client with CN: %s", req.Name) + dnsNames := []string{req.Name} + cert, pk, err := s.CertificateService.CreateCertificateSimple(req.Name, defaultCertLifetime, dnsNames) + if err != nil { + return errors.Wrapf(err, "failed to create envoy certificate") + } + req.operationLog.Info().Msgf("got certificate with serial number: %v for CN: %s", cert.SerialNumber, cert.Subject.CommonName) + err = createEnvoySecret(endpointMgr.K8sClient(), req.Namespace, req.caCert, cert, pk) + if err != nil { + return err + } + if err := s.CertificateService.CreateBinding(cert, entity.CertificateBindingServiceTypeOpenFLEnvoy, envoy.UUID, req.federation.UUID, entity.FederationTypeOpenFL); err != nil { + return err + } + envoy.CertConfig.EnvoyClientCertInfo.UUID = cert.UUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to update envoy cert info") + } + req.operationLog.Info().Msgf("certificate prepared") + } + + jobUUID, err := kfClient.SubmitClusterInstallationJob(envoy.DeploymentYAML) + if err != nil { + return errors.Wrapf(err, "fail to submit envoy creation request") + } + envoy.JobUUID = jobUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to update envoy's job uuid") + } + clusterUUID, err := kfClient.WaitClusterUUID(jobUUID) + if err != nil { + return errors.Wrapf(err, "fail to get cluster uuid") + } + envoy.ClusterUUID = clusterUUID + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to update cluster uuid") + } + req.operationLog.Info().Msgf("job submitted and running: jobUUID: %s, clusterUUID: %s", jobUUID, clusterUUID) + + job, err := kfClient.WaitJob(jobUUID) + if err != nil { + return err + } + if job.Status != modules.JobStatusSuccess { + return errors.Errorf("job is %s, job info: %v", job.Status.String(), job) + } + envoy.Status = entity.ParticipantOpenFLStatusActive + if err := s.ParticipantOpenFLRepo.UpdateInfoByUUID(envoy); err != nil { + return errors.Wrap(err, "failed to save cluster info") + } + req.operationLog.Info().Msgf("envoy deployed and running, name: %s, uuid: %s", envoy.UUID, envoy.Name) + return nil +} + +var ( + createDirectorSecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, + directorCert *entity.Certificate, directorKey *rsa.PrivateKey) error { + certBytes, err := directorCert.EncodePEM() + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: entity.ParticipantOpenFLSecretNameDirector, + }, + Data: map[string][]byte{ + "priv.key": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(directorKey), + }), + "director.crt": certBytes, + "root_ca.crt": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } + + createJupyterSecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, + jupyterCert *entity.Certificate, jupyterKey *rsa.PrivateKey) error { + certBytes, err := jupyterCert.EncodePEM() + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: entity.ParticipantOpenFLSecretNameJupyter, + }, + Data: map[string][]byte{ + "priv.key": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(jupyterKey), + }), + "notebook.crt": certBytes, + "root_ca.crt": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } + + createEnvoySecret = func(client kubernetes.Client, namespace string, caCert *x509.Certificate, + cert *entity.Certificate, key *rsa.PrivateKey) error { + certBytes, err := cert.EncodePEM() + if err != nil { + return err + } + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: entity.ParticipantOpenFLSecretNameEnvoy, + }, + Data: map[string][]byte{ + "priv.key": pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(key), + }), + "envoy.crt": certBytes, + "root_ca.crt": pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: caCert.Raw, + }), + }, + } + return createSecret(client, namespace, secret) + } + + createEnvoyShardDescriptor = func(client kubernetes.Client, namespace string, pythonFiles map[string]string) error { + cm := &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Name: "envoy-python-configs", + }, + Data: pythonFiles, + } + return createConfigMap(client, namespace, cm) + } +) diff --git a/server/domain/service/participant_service.go b/server/domain/service/participant_service.go new file mode 100644 index 00000000..d96f60ef --- /dev/null +++ b/server/domain/service/participant_service.go @@ -0,0 +1,294 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "context" + "crypto/rsa" + "regexp" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/pkg/kubefate" + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + corev1 "k8s.io/api/core/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +// This means the CA should support issuing certificate valid for at least 1 year +const defaultCertLifetime = time.Hour * 24 * 365 + +// ParticipantService provides functions to manage participants +type ParticipantService struct { + FederationRepo repo.FederationRepository + ChartRepo repo.ChartRepository + EventService EventServiceInt + CertificateService ParticipantCertificateServiceInt + EndpointService ParticipantEndpointServiceInt +} + +func (s *ParticipantService) buildKubeFATEMgrAndClient(endpointUUID string) (kubefate.ClientManager, kubefate.Client, func(), error) { + var closer func() + endpointMgr, err := s.EndpointService.buildKubeFATEClientManagerFromEndpointUUID(endpointUUID) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed to get endpoint manager") + } + kfClient, err := endpointMgr.BuildClient() + if err != nil { + kfClient, closer, err = endpointMgr.BuildPFClient() + if err != nil { + return nil, nil, closer, errors.Wrapf(err, "failed to get kubefate client") + } + } + return endpointMgr, kfClient, closer, nil +} + +// ParticipantDeploymentBaseInfo contains basic deployment information for a participant +type ParticipantDeploymentBaseInfo struct { + Description string `json:"description"` + EndpointUUID string `json:"endpoint_uuid"` + DeploymentYAML string `json:"deployment_yaml"` +} + +// For mocking purpose +var ( + getServiceAccessWithFallback = func(client kubernetes.Client, namespace, serviceName, portName string, lbFallbackToNodePort bool) (serviceType corev1.ServiceType, host string, port int, err error) { + log.Info().Msgf("retrieving address for service: %s, port: %s in namespace: %s", serviceName, portName, namespace) + service, err := client.GetClientSet().CoreV1().Services(namespace).Get(context.TODO(), serviceName, v1.GetOptions{}) + if err != nil { + return + } + + serviceYAML, _ := yaml.Marshal(service) + log.Debug().Msgf("service yaml: %s", serviceYAML) + + serviceType = service.Spec.Type + host = service.Spec.ClusterIP + port = 0 + nodePort := 0 + for _, p := range service.Spec.Ports { + if p.Name == portName { + port = int(p.Port) + nodePort = int(p.NodePort) + } + } + + getNodePortServiceAccess := func() { + nl, _ := client.GetClientSet().CoreV1().Nodes().List(context.TODO(), v1.ListOptions{}) + node := nl.Items[0] + for _, addr := range node.Status.Addresses { + if addr.Type == corev1.NodeExternalIP { + host = addr.Address + break + } else if addr.Type == corev1.NodeInternalIP { + host = addr.Address + } + } + port = nodePort + } + + switch serviceType { + case corev1.ServiceTypeNodePort: + getNodePortServiceAccess() + case corev1.ServiceTypeLoadBalancer: + retry := 5 + for { + service, err := client.GetClientSet().CoreV1().Services(namespace).Get(context.TODO(), serviceName, v1.GetOptions{}) + serviceYAML, _ := yaml.Marshal(service) + log.Debug().Msgf("service yaml: %s", serviceYAML) + if err != nil || len(service.Status.LoadBalancer.Ingress) == 0 { + retry-- + if retry > 0 { + log.Warn().Msgf("failed to get LoadBalancer address, retrying (%v remaining)", retry) + time.Sleep(time.Second * 20) + continue + } else { + break + } + } + host = service.Status.LoadBalancer.Ingress[0].IP + if host == "" { + host = service.Status.LoadBalancer.Ingress[0].Hostname + } + break + } + if retry == 0 { + if lbFallbackToNodePort { + log.Info().Msg("fallback to acquiring the service address as type NodePort") + serviceType = corev1.ServiceTypeNodePort + getNodePortServiceAccess() + } else { + err = errors.New("failed to get load balancer address in time") + return + } + } + } + log.Info().Msgf("%s(%s) type: %v, host: %s, port: %v", serviceName, portName, serviceType, host, port) + if port == 0 || host == "" { + err = errors.Wrapf(err, "failed to get port number or host address") + } + return + } + + ensureNSExisting = func(client kubernetes.Client, namespace string) (bool, error) { + created := false + _, err := client.GetClientSet().CoreV1().Namespaces().Get(context.TODO(), namespace, v1.GetOptions{}) + if err != nil { + if apierr.IsNotFound(err) { + log.Info().Msgf("creating namespace %s", namespace) + _, err := client.GetClientSet().CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: namespace, + }, + }, v1.CreateOptions{}) + if err != nil { + return false, errors.Wrapf(err, "failed to create namespace %s", namespace) + } + created = true + } else { + return created, errors.Wrapf(err, "failed to query namespace %s", namespace) + } + } + return created, nil + } + + getIngressInfo = func(client kubernetes.Client, name, namespace string) (*entity.ParticipantFATEIngress, error) { + ingress, err := client.GetClientSet().NetworkingV1().Ingresses(namespace).Get(context.TODO(), name, v1.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get ingress info") + } + if len(ingress.Spec.Rules) == 0 { + return nil, errors.New("ingress is not available") + } + ingressInfo := &entity.ParticipantFATEIngress{ + Hosts: nil, + Addresses: nil, + TLS: ingress.Spec.TLS != nil, + } + for _, rule := range ingress.Spec.Rules { + ingressInfo.Hosts = append(ingressInfo.Hosts, rule.Host) + } + for _, lbIngress := range ingress.Status.LoadBalancer.Ingress { + if lbIngress.Hostname != "" { + ingressInfo.Addresses = append(ingressInfo.Addresses, lbIngress.Hostname) + } + if lbIngress.IP != "" { + ingressInfo.Addresses = append(ingressInfo.Addresses, lbIngress.IP) + } + } + return ingressInfo, nil + } + + deletePodWithPrefix = func(client kubernetes.Client, namespace, podPrefix string) error { + var err error + pods, _ := client.GetClientSet().CoreV1().Pods(namespace).List(context.TODO(), v1.ListOptions{}) + for _, pod := range pods.Items { + if strings.HasPrefix(pod.Name, podPrefix) { + if deleteErr := client.GetClientSet().CoreV1().Pods(namespace).Delete(context.TODO(), pod.Name, v1.DeleteOptions{}); deleteErr != nil { + log.Err(deleteErr).Msgf("failed to delete pod") + err = deleteErr + } + } + } + return err + } + + createSecret = func(client kubernetes.Client, namespace string, secret *corev1.Secret) error { + _, err := client.GetClientSet().CoreV1().Secrets(namespace).Get(context.TODO(), secret.Name, v1.GetOptions{}) + if err == nil { + log.Warn().Msgf("deleting stale secret with name %s in namespace %s", secret.Name, namespace) + err := client.GetClientSet().CoreV1().Secrets(namespace).Delete(context.TODO(), secret.Name, v1.DeleteOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to delete stale secret") + } + } else if !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to check secret existence") + } + _, err = client.GetClientSet().CoreV1().Secrets(namespace).Create(context.TODO(), secret, v1.CreateOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to create secret") + } + return nil + } + + createConfigMap = func(client kubernetes.Client, namespace string, cm *corev1.ConfigMap) error { + _, err := client.GetClientSet().CoreV1().ConfigMaps(namespace).Get(context.TODO(), cm.Name, v1.GetOptions{}) + if err == nil { + log.Warn().Msgf("deleting stale configmap with name %s in namespace %s", cm.Name, namespace) + err := client.GetClientSet().CoreV1().ConfigMaps(namespace).Delete(context.TODO(), cm.Name, v1.DeleteOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to delete stale configmap") + } + } else if !apierr.IsNotFound(err) { + return errors.Wrapf(err, "failed to check configmap existence") + } + _, err = client.GetClientSet().CoreV1().ConfigMaps(namespace).Create(context.TODO(), cm, v1.CreateOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to create configmap") + } + return nil + } + + createRegistrySecret = func(client kubernetes.Client, secretName string, namespace string, registrySecretConfig valueobject.KubeRegistrySecretConfig) error { + secret, err := registrySecretConfig.BuildKubeSecret(secretName, namespace) + if err != nil { + return err + } + return createSecret(client, namespace, secret) + } +) + +func getServiceAccess(client kubernetes.Client, namespace, serviceName, portName string) (corev1.ServiceType, string, int, error) { + return getServiceAccessWithFallback(client, namespace, serviceName, portName, false) +} + +func toDeploymentName(name string) string { + // replace non-alphanumeric character with space + name = strings.ToLower(name) + re := regexp.MustCompile(`[^a-z0-9]`) + name = re.ReplaceAllString(name, " ") + + // trim spaces and replace with dash + name = strings.TrimSpace(name) + re = regexp.MustCompile(`\s+`) + name = re.ReplaceAllString(name, " ") + name = strings.ReplaceAll(name, " ", "-") + + return name +} + +// ParticipantCertificateServiceInt declares the methods of a certificate service that this service needs. +// "Caller defines interfaces" +type ParticipantCertificateServiceInt interface { + DefaultCA() (*entity.CertificateAuthority, error) + CreateCertificateSimple(commonName string, lifetime time.Duration, dnsNames []string) (cert *entity.Certificate, pk *rsa.PrivateKey, err error) + CreateBinding(cert *entity.Certificate, serviceType entity.CertificateBindingServiceType, participantUUID string, federationUUID string, federationType entity.FederationType) error + RemoveBinding(participantUUID string) error +} + +// ParticipantEndpointServiceInt declares the methods of an endpoint service that participant service needs +type ParticipantEndpointServiceInt interface { + TestKubeFATE(uuid string) error + + buildKubeFATEClientManagerFromEndpointUUID(uuid string) (kubefate.ClientManager, error) + ensureEndpointExist(infraUUID string) (string, error) +} diff --git a/server/domain/service/user_service.go b/server/domain/service/user_service.go new file mode 100644 index 00000000..7567edf3 --- /dev/null +++ b/server/domain/service/user_service.go @@ -0,0 +1,45 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/rs/zerolog/log" + "golang.org/x/crypto/bcrypt" +) + +// UserService provides common services to work with entity.User +type UserService struct { + Repo repo.UserRepository +} + +// LoginService validates the provided username and password and returns the user entity when succeeded +func (s *UserService) LoginService(username, password string) (*entity.User, error) { + user := &entity.User{Name: username} + if err := func() error { + if err := s.Repo.LoadByName(user); err != nil { + return err + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return err + } + return nil + }(); err != nil { + log.Warn().Err(err).Msgf("failed to validate password for user: %s", username) + return nil, err + } + return user, nil +} diff --git a/server/domain/utils/domain.go b/server/domain/utils/domain.go new file mode 100644 index 00000000..1ddcf25f --- /dev/null +++ b/server/domain/utils/domain.go @@ -0,0 +1,24 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + _ "net" + _ "unsafe" +) + +//go:linkname IsDomainName net.isDomainName +// IsDomainName checks if a string is a presentation-format domain name +func IsDomainName(s string) bool diff --git a/server/domain/utils/encrypt.go b/server/domain/utils/encrypt.go new file mode 100644 index 00000000..27c4ffa4 --- /dev/null +++ b/server/domain/utils/encrypt.go @@ -0,0 +1,133 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" + "strings" + + "github.com/spf13/viper" +) + +const ( + encryptHeaderV1 = "" +) + +// Encrypt encrypts the passed secret using AES with configured secret key +func Encrypt(secret string) (string, error) { + if len(secret) == 0 { + return secret, nil + } + secretKey, _ := findSecretKey() + encrypted, err := reversibleEncrypt(secret, secretKey) + if err != nil { + return "", err + } + return encrypted, nil +} + +// Decrypt decrypts the passed secret using configured secret key +func Decrypt(secret string) (string, error) { + if len(secret) == 0 { + return "", nil + } + secretKey, _ := findSecretKey() + decrypted, err := reversibleDecrypt(secret, secretKey) + if err != nil { + return "", err + } + return decrypted, nil +} + +func findSecretKey() (string, error) { + // TODO: check key size as AES only supports key sizes of 16, 24 or 32 bytes + secretKey := viper.GetString("lifecyclemanager.secretkey") + if secretKey == "" { + secretKey = "passphrase123456" + } + return secretKey, nil +} + +// reversibleEncrypt encrypts the str with aes/base64 +func reversibleEncrypt(str, key string) (string, error) { + keyBytes := []byte(key) + var block cipher.Block + var err error + + if block, err = aes.NewCipher(keyBytes); err != nil { + return "", err + } + + // ensures the value is no larger than 64 MB, which fits comfortably within an int and avoids the overflow + if len(str) > 64*1024*1024 { + return "", errors.New("str value too large") + } + + size := aes.BlockSize + len(str) + cipherText := make([]byte, size) + iv := cipherText[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(cipherText[aes.BlockSize:], []byte(str)) + encrypted := encryptHeaderV1 + base64.RawURLEncoding.EncodeToString(cipherText) + return encrypted, nil +} + +// reversibleDecrypt decrypts the str with aes/base64 or base 64 depending on "header" +func reversibleDecrypt(str, key string) (string, error) { + if strings.HasPrefix(str, encryptHeaderV1) { + str = str[len(encryptHeaderV1):] + return decryptAES(str, key) + } + // fallback to base64 + return decodeB64(str) +} + +func decodeB64(str string) (string, error) { + cipherText, err := base64.RawURLEncoding.DecodeString(str) + return string(cipherText), err +} + +func decryptAES(str, key string) (string, error) { + keyBytes := []byte(key) + var block cipher.Block + var cipherText []byte + var err error + + if block, err = aes.NewCipher(keyBytes); err != nil { + return "", err + } + if cipherText, err = base64.RawURLEncoding.DecodeString(str); err != nil { + return "", err + } + if len(cipherText) < aes.BlockSize { + err = errors.New("cipherText too short") + return "", err + } + + iv := cipherText[:aes.BlockSize] + cipherText = cipherText[aes.BlockSize:] + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(cipherText, cipherText) + return string(cipherText), nil +} diff --git a/server/domain/utils/placeholder.s b/server/domain/utils/placeholder.s new file mode 100644 index 00000000..1dcf1a3f --- /dev/null +++ b/server/domain/utils/placeholder.s @@ -0,0 +1,15 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Placeholder because we need to use unexported functions via go:linkname \ No newline at end of file diff --git a/server/domain/valueobject/kube_registry_config.go b/server/domain/valueobject/kube_registry_config.go new file mode 100644 index 00000000..71fbb653 --- /dev/null +++ b/server/domain/valueobject/kube_registry_config.go @@ -0,0 +1,118 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + b64 "encoding/base64" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// KubeRegistryConfig contains registry configurations that can be used in K8s clusters +type KubeRegistryConfig struct { + UseRegistry bool `json:"use_registry" form:"use_registry" yaml:"useRegistry"` + Registry string `json:"registry" form:"registry" yaml:"registry"` + UseRegistrySecret bool `json:"use_registry_secret" form:"use_registry_secret" yaml:"useRegistrySecret"` + RegistrySecretConfig KubeRegistrySecretConfig `json:"registry_secret_config" yaml:"registrySecretConfig"` +} + +func (c KubeRegistryConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *KubeRegistryConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// KubeRegistrySecretConfig is the secret configuration that can be used to generate K8s imagePullSecret +type KubeRegistrySecretConfig struct { + ServerURL string `json:"server_url" yaml:"serverURL"` + Username string `json:"username" yaml:"username"` + Password string `json:"password" yaml:"password"` +} + +// BuildKubeSecret create a K8s secret containing the `.dockerconfigjson` for authenticating with remote registry +func (c KubeRegistrySecretConfig) BuildKubeSecret(name, namespace string) (*corev1.Secret, error) { + // .dockerconfigjson auth is base64 encoded with format 'username:password' + auth := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.Username, c.Password))) + secretObj := &dockerConfigAuth{ + AuthMap: map[string]registryCredentials{ + c.ServerURL: { + Username: c.Username, + Password: c.Password, + Auth: auth, + }, + }, + } + secretData, err := json.Marshal(secretObj) + if err != nil { + return nil, err + } + return &corev1.Secret{ + TypeMeta: v1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: "kubernetes.io/dockerconfigjson", + Data: map[string][]byte{ + ".dockerconfigjson": secretData, + }, + }, nil +} + +// BuildSecretB64String generates the authentication string +func (c KubeRegistrySecretConfig) BuildSecretB64String() (string, error) { + // .dockerconfigjson auth is base64 encoded with format 'username:password' + auth := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.Username, c.Password))) + secretObj := &dockerConfigAuth{ + AuthMap: map[string]registryCredentials{ + c.ServerURL: { + Username: c.Username, + Password: c.Password, + Auth: auth, + }, + }, + } + secretData, err := json.Marshal(secretObj) + if err != nil { + return "", err + } + secretStr := b64.StdEncoding.EncodeToString([]byte(secretData)) + return secretStr, nil +} + +// dockerConfigAuth is the dockerconfig containing only the auths filed +type dockerConfigAuth struct { + AuthMap registryAuthMap `json:"auths"` +} + +// registryAuthMap is a map of registries to their credentials +type registryAuthMap map[string]registryCredentials + +// registryCredentials defines the fields stored per registry in the dockerconfig auths map +type registryCredentials struct { + Username string `json:"username"` + Password string `json:"password"` + Auth string `json:"auth"` +} diff --git a/server/domain/valueobject/kubeconfig.go b/server/domain/valueobject/kubeconfig.go new file mode 100644 index 00000000..98a7b32f --- /dev/null +++ b/server/domain/valueobject/kubeconfig.go @@ -0,0 +1,73 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "crypto/sha256" + "database/sql/driver" + "encoding/json" + "fmt" + + "github.com/FederatedAI/FedLCM/pkg/kubernetes" + "github.com/rs/zerolog/log" +) + +// KubeConfig contains necessary information needed to work with a kubernetes cluster +// Currently only the kubeconfig content is included +type KubeConfig struct { + KubeConfigContent string `json:"kubeconfig_content"` +} + +func (c KubeConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *KubeConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +// Validate checks if the config can be used to connect to a K8s cluster +func (c *KubeConfig) Validate() error { + client, err := kubernetes.NewKubernetesClient("", c.KubeConfigContent) + if err != nil { + return err + } + versionInfo, err := client.GetClientSet().Discovery().ServerVersion() + if err != nil { + return err + } + log.Info().Msgf("got k8s server version: %s", versionInfo.String()) + // TODO: check other conditions like permissions, ingress installation status etc. + return nil +} + +// APIHost returns the address for the API server connection +func (c *KubeConfig) APIHost() (string, error) { + client, err := kubernetes.NewKubernetesClient("", c.KubeConfigContent) + if err != nil { + return "", err + } + config, err := client.GetConfig() + if err != nil { + return "", err + } + return config.Host, nil +} + +// SHA2565 hashes the kubeconfig content and returns the hash string +func (c *KubeConfig) SHA2565() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(c.KubeConfigContent))) +} diff --git a/server/domain/valueobject/labels.go b/server/domain/valueobject/labels.go new file mode 100644 index 00000000..bcea29af --- /dev/null +++ b/server/domain/valueobject/labels.go @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + "encoding/json" +) + +// Labels is essentially a key-value string map +type Labels map[string]string + +func (c Labels) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *Labels) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} diff --git a/server/domain/valueobject/shard_descriptor_config.go b/server/domain/valueobject/shard_descriptor_config.go new file mode 100644 index 00000000..bd73e7a3 --- /dev/null +++ b/server/domain/valueobject/shard_descriptor_config.go @@ -0,0 +1,37 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + "encoding/json" +) + +// ShardDescriptorConfig contains configurations for OpenFL shards +type ShardDescriptorConfig struct { + SampleShape []string `json:"sample_shape"` + TargetShape []string `json:"target_shape"` + EnvoyConfigYaml string `json:"envoy_config_yaml"` + PythonFiles map[string]string `json:"python_files"` +} + +func (c ShardDescriptorConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *ShardDescriptorConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} diff --git a/server/infrastructure/gorm/certificate_authority_repo.go b/server/infrastructure/gorm/certificate_authority_repo.go new file mode 100644 index 00000000..c3b48ce0 --- /dev/null +++ b/server/infrastructure/gorm/certificate_authority_repo.go @@ -0,0 +1,65 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// CertificateAuthorityRepo is the implementation of the repo.CertificateAuthorityRepository interface +type CertificateAuthorityRepo struct{} + +var _ repo.CertificateAuthorityRepository = (*CertificateAuthorityRepo)(nil) + +// ErrCertificateAuthorityExist means new CA cannot be created due to the existence of the same-name CA +var ErrCertificateAuthorityExist = errors.New("CA already exists") + +func (r *CertificateAuthorityRepo) Create(instance interface{}) error { + ca := instance.(*entity.CertificateAuthority) + + var count int64 + db.Model(&entity.CertificateAuthority{}).Count(&count) + if count > 0 { + return ErrCertificateAuthorityExist + } + + if err := db.Create(ca).Error; err != nil { + return err + } + return nil +} + +func (r *CertificateAuthorityRepo) UpdateByUUID(instance interface{}) error { + ca := instance.(*entity.CertificateAuthority) + return db.Where("uuid = ?", ca.UUID). + Select("name", "description", "type", "config_json").Updates(ca).Error +} + +func (r *CertificateAuthorityRepo) GetFirst() (interface{}, error) { + ca := &entity.CertificateAuthority{} + if err := db.First(ca).Error; err != nil { + return nil, err + } + return ca, nil +} + +// InitTable makes sure the table is created in the db +func (r *CertificateAuthorityRepo) InitTable() { + if err := db.AutoMigrate(entity.CertificateAuthority{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/certificate_binding_repo.go b/server/infrastructure/gorm/certificate_binding_repo.go new file mode 100644 index 00000000..9494fb06 --- /dev/null +++ b/server/infrastructure/gorm/certificate_binding_repo.go @@ -0,0 +1,63 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// CertificateBindingRepo is the implementation of the repo.CertificateBindingRepository interface +type CertificateBindingRepo struct{} + +var _ repo.CertificateBindingRepository = (*CertificateBindingRepo)(nil) + +func (r *CertificateBindingRepo) ListByParticipantUUID(participantUUID string) (interface{}, error) { + var bindings []entity.CertificateBinding + err := db.Where("participant_uuid = ?", participantUUID).Find(&bindings).Error + if err != nil { + return 0, err + } + return bindings, nil +} + +func (r *CertificateBindingRepo) Create(instance interface{}) error { + binding := instance.(*entity.CertificateBinding) + + if err := db.Create(binding).Error; err != nil { + return err + } + return nil +} + +func (r *CertificateBindingRepo) ListByCertificateUUID(certificateUUID string) (interface{}, error) { + var bindings []entity.CertificateBinding + err := db.Where("certificate_uuid = ?", certificateUUID).Find(&bindings).Error + if err != nil { + return 0, err + } + return bindings, nil +} + +func (r *CertificateBindingRepo) DeleteByParticipantUUID(participantUUID string) error { + return db.Unscoped().Where("participant_uuid = ?", participantUUID).Delete(&entity.CertificateBinding{}).Error +} + +// InitTable makes sure the table is created in the db +func (r *CertificateBindingRepo) InitTable() { + if err := db.AutoMigrate(entity.CertificateBinding{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/certificate_repo.go b/server/infrastructure/gorm/certificate_repo.go new file mode 100644 index 00000000..7d34a038 --- /dev/null +++ b/server/infrastructure/gorm/certificate_repo.go @@ -0,0 +1,69 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// CertificateRepo is the implementation of the repo.CertificateRepository interface +type CertificateRepo struct{} + +var _ repo.CertificateRepository = (*CertificateRepo)(nil) + +func (r *CertificateRepo) GetBySerialNumber(serialNumberStr string) (interface{}, error) { + cert := &entity.Certificate{} + if err := db.Where("serial_number_str = ?", serialNumberStr).First(cert).Error; err != nil { + return nil, err + } + return cert, nil +} + +func (r *CertificateRepo) Create(instance interface{}) error { + cert := instance.(*entity.Certificate) + + if err := db.Create(cert).Error; err != nil { + return err + } + return nil +} + +func (r *CertificateRepo) List() (interface{}, error) { + var certs []entity.Certificate + if err := db.Find(&certs).Error; err != nil { + return nil, err + } + return certs, nil +} + +func (r *CertificateRepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.Certificate{}).Error +} + +func (r *CertificateRepo) GetByUUID(uuid string) (interface{}, error) { + cert := &entity.Certificate{} + if err := db.Where("uuid = ?", uuid).First(cert).Error; err != nil { + return nil, err + } + return cert, nil +} + +// InitTable makes sure the table is created in the db +func (r *CertificateRepo) InitTable() { + if err := db.AutoMigrate(entity.Certificate{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/chart_mock_repo.go b/server/infrastructure/gorm/chart_mock_repo.go new file mode 100644 index 00000000..86d81096 --- /dev/null +++ b/server/infrastructure/gorm/chart_mock_repo.go @@ -0,0 +1,3349 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "time" + + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/FederatedAI/FedLCM/server/infrastructure/gorm/mock" + "github.com/pkg/errors" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +// ChartMockRepo is the implementation of the repo.ChartRepository interface +// The data is hard-coded for now as the template to generate the final yaml content +// is hard to be customized by the end users. +type ChartMockRepo struct{} + +var _ repo.ChartRepository = (*ChartMockRepo)(nil) + +func (r *ChartMockRepo) Create(instance interface{}) error { + panic("implement me") +} + +func (r *ChartMockRepo) List() (interface{}, error) { + var chartList []entity.Chart + for uuid, _ := range chartMap { + if chartMap[uuid].Type == entity.ChartTypeOpenFLDirector || + chartMap[uuid].Type == entity.ChartTypeOpenFLEnvoy { + if !viper.GetBool("lifecyclemanager.experiment.enabled") { + continue + } + } + chartList = append(chartList, chartMap[uuid]) + } + return chartList, nil +} + +func (r *ChartMockRepo) DeleteByUUID(uuid string) error { + panic("implement me") +} + +func (r *ChartMockRepo) GetByUUID(uuid string) (interface{}, error) { + if chart, ok := chartMap[uuid]; !ok { + return nil, errors.New("chart not found") + } else { + return &chart, nil + } +} + +func (r *ChartMockRepo) ListByType(instance interface{}) (interface{}, error) { + t := instance.(entity.ChartType) + var chartList []entity.Chart + for uuid, chart := range chartMap { + if t == chart.Type { + chartList = append(chartList, chartMap[uuid]) + } + } + return chartList, nil +} + +var ( + chartMap = map[string]entity.Chart{ + "4ad46829-a827-4632-b169-c8675360321e": { + Model: gorm.Model{ + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "4ad46829-a827-4632-b169-c8675360321e", + Name: "chart for FATE exchange v1.8.0", + Description: "This chart is for deploying FATE exchange v1.8.0", + Type: entity.ChartTypeFATEExchange, + ChartName: "fate-exchange", + Version: "v1.8.0", + AppVersion: "v1.8.0", + Chart: `apiVersion: v1 +appVersion: v1.8.0 +description: A Helm chart for fate exchange +name: fate-exchange +version: v1.8.0`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: fate-exchange +chartVersion: v1.8.0 +partyId: 0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +imageTag: "1.8.0-release" +# pullPolicy: +# persistence: false +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} +modules: + - trafficServer + - nginx + +trafficServer: + type: {{.ServiceType}} + route_table: + sni: + # replicas: 1 + # nodeSelector: + # tolerations: + # affinity: + # nodePort: + # loadBalancerIP: + +nginx: + type: {{.ServiceType}} + route_table: + # replicas: 1 + # nodeSelector: + # tolerations: + # affinity: + # httpNodePort: + # grpcNodePort: + # loadBalancerIP: `, + Values: `partyId: 1 +partyName: fate-exchange + +image: + registry: federatedai + isThridParty: + tag: 1.8.0-release + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +partyId: 9999 +partyName: fate-9999 + +podSecurityPolicy: + enabled: false + +partyList: +- partyId: 8888 + partyIp: 192.168.8.1 + partyPort: 30081 +- partyId: 10000 + partyIp: 192.168.10.1 + partyPort: 30101 + +modules: + rollsite: + include: false + ip: rollsite + type: ClusterIP + nodePort: 30001 + loadBalancerIP: + nodeSelector: + tolerations: + affinity: + # partyList is used to configure the cluster information of all parties that join in the exchange deployment mode. (When eggroll was used as the calculation engine at the time) + partyList: + # - partyId: 8888 + # partyIp: 192.168.8.1 + # partyPort: 30081 + # - partyId: 10000 + # partyIp: 192.168.10.1 + # partyPort: 30101 + nginx: + include: false + type: NodePort + httpNodePort: 30003 + grpcNodePort: 30008 + loadBalancerIP: + nodeSelector: + tolerations: + affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + # 10000: + # fateflow: + # - grpc_port: 30102 + # host: 192.168.10.1 + # http_port: 30107 + # proxy: + # - grpc_port: 30108 + # host: 192.168.10.1 + # http_port: 30103 + # 9999: + # fateflow: + # - grpc_port: 30092 + # host: 192.168.9.1 + # http_port: 30097 + # proxy: + # - grpc_port: 30098 + # host: 192.168.9.1 + # http_port: 30093 + trafficServer: + include: false + type: ClusterIP + nodePort: 30007 + loadBalancerIP: + nodeSelector: + tolerations: + affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + # sni: + # - fqdn: 10000.fate.org + # tunnelRoute: 192.168.0.2:30109 + # - fqdn: 9999.fate.org + # tunnelRoute: 192.168.0.3:30099`, + ValuesTemplate: `partyId: {{ .partyId }} +partyName: {{ .name }} + +image: + registry: {{ .registry | default "federatedai" }} + isThridParty: {{ empty .registry | ternary "false" "true" }} + tag: {{ .imageTag | default "1.8.0-release" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +exchange: +{{- with .rollsite }} +{{- with .exchange }} + partyIp: {{ .ip }} + partyPort: {{ .port }} +{{- end }} +{{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +partyList: +{{- with .rollsite }} +{{- range .partyList }} + - partyId: {{ .partyId }} + partyIp: {{ .partyIp }} + partyPort: {{ .partyPort }} +{{- end }} +{{- end }} + +modules: + rollsite: + include: {{ has "rollsite" .modules }} + {{- with .rollsite }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type }} + nodePort: {{ .nodePort }} + partyList: + {{- range .partyList }} + - partyId: {{ .partyId }} + partyIp: {{ .partyIp }} + partyPort: {{ .partyPort }} + {{- end }} + {{- end }} + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type }} + replicas: {{ .replicas }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + trafficServer: + include: {{ has "trafficServer" .modules }} + {{- with .trafficServer }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type }} + replicas: {{ .replicas }} + nodePort: {{ .nodePort }} + route_table: + sni: + {{- range .route_table.sni }} + - fqdn: {{ .fqdn }} + tunnelRoute: {{ .tunnelRoute }} + {{- end }} + {{- end }}`, + ArchiveContent: nil, + Private: false, + }, + "7a51112a-b0ad-4c26-b2c0-1e6f7eca6073": { + Model: gorm.Model{ + ID: 2, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "7a51112a-b0ad-4c26-b2c0-1e6f7eca6073", + Name: "chart for FATE cluster v1.8.0", + Description: "This is chart for installing FATE cluster v1.8.0", + Type: entity.ChartTypeFATECluster, + ChartName: "fate", + Version: "v1.8.0", + AppVersion: "v1.8.0", + Chart: `apiVersion: v1 +appVersion: v1.8.0 +description: A Helm chart for fate-training +name: fate +version: v1.8.0 +home: https://fate.fedai.org +icon: https://aisp-1251170195.cos.ap-hongkong.myqcloud.com/wp-content/uploads/sites/12/2019/09/logo.png +sources: + - https://github.com/FederatedAI/KubeFATE + - https://github.com/FederatedAI/FATE`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: fate +chartVersion: v1.8.0 +partyId: {{.PartyID}} +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +# imageTag: "1.8.0-release" +persistence: {{ .EnablePersistence }} +# pullPolicy: +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} + +# ingressClassName: nginx + +modules: + - mysql + - python + - fateboard + - client + - spark + - hdfs + - nginx + - pulsar + +backend: spark_pulsar + +ingress: + fateboard: + hosts: + - name: {{.Name}}.fateboard.{{.Domain}} + client: + hosts: + - name: {{.Name}}.notebook.{{.Domain}} + spark: + hosts: + - name: {{.Name}}.spark.{{.Domain}} + pulsar: + hosts: + - name: {{.Name}}.pulsar.{{.Domain}} + +nginx: + type: {{.ServiceType}} + exchange: + ip: {{.ExchangeNginxHost}} + httpPort: {{.ExchangeNginxPort}} + # nodeSelector: + # tolerations: + # affinity: + # loadBalancerIP: + # httpNodePort: 30093 + # grpcNodePort: 30098 + +pulsar: + publicLB: + enabled: true + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} + size: 1Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # httpNodePort: 30094 + # httpsNodePort: 30099 + # loadBalancerIP: + +mysql: + size: 1Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + subPath: "mysql" + # nodeSelector: + # tolerations: + # affinity: + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + +python: + size: 10Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + # httpNodePort: + # grpcNodePort: + # loadBalancerIP: + # serviceAccountName: "" + # nodeSelector: + # tolerations: + # affinity: + # resources: + # logLevel: INFO + # spark: + # cores_per_node: 20 + # nodes: 2 + # master: spark://spark-master:7077 + # driverHost: + # driverHostType: + # portMaxRetries: + # driverStartPort: + # blockManagerStartPort: + # pysparkPython: + # hdfs: + # name_node: hdfs://namenode:9000 + # path_prefix: + # pulsar: + # host: pulsar + # mng_port: 8080 + # port: 6650 + # nginx: + # host: nginx + # http_port: 9300 + # grpc_port: 9310 + +client: + size: 1Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + subPath: "client" + # nodeSelector: + # tolerations: + # affinity: + +hdfs: + namenode: + storageClass: {{ .StorageClass }} + size: 3Gi + existingClaim: "" + accessMode: ReadWriteOnce + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + datanode: + size: 10Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + +spark: + # master: + # replicas: 1 + # resources: + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30977 + worker: + replicas: 2 + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +`, + Values: `image: + registry: federatedai + isThridParty: + tag: 1.8.0-release + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +partyId: 9999 +partyName: fate-9999 + +istio: + enabled: false + +podSecurityPolicy: + enabled: false + +ingressClassName: nginx + +ingress: + fateboard: + # annotations: + hosts: + - name: fateboard.example.com + path: / + tls: [] + # - secretName: my-tls-secret + # hosts: + # - fateboard.example.com + client: + # annotations: + hosts: + - name: notebook.example.com + path: / + tls: [] + spark: + # annotations: + hosts: + - name: spark.example.com + path: / + tls: [] + rabbitmq: + # annotations: + hosts: + - name: rabbitmq.example.com + path: / + tls: [] + pulsar: + # annotations: + hosts: + - name: pulsar.example.com + path: / + tls: [] + +exchange: + partyIp: 192.168.1.1 + partyPort: 30001 + +exchangeList: +- id: 9991 + ip: 192.168.1.1 + port: 30910 + +partyList: +- partyId: 8888 + partyIp: 192.168.8.1 + partyPort: 30081 +- partyId: 10000 + partyIp: 192.168.10.1 + partyPort: 30101 + +persistence: + enabled: false + +modules: + rollsite: + include: true + ip: rollsite + type: ClusterIP + nodePort: 30091 + loadBalancerIP: + nodeSelector: + tolerations: + affinity: + polling: + enabled: false + + # type: client + # server: + # ip: 192.168.9.1 + # port: 9370 + + # type: server + # clientList: + # - partID: 9999 + # concurrency: 50 + + lbrollsite: + include: true + ip: rollsite + type: ClusterIP + nodePort: 30091 + loadBalancerIP: + size: "2M" + nodeSelector: + tolerations: + affinity: + python: + include: true + type: ClusterIP + httpNodePort: 30097 + grpcNodePort: 30092 + loadBalancerIP: + serviceAccountName: + nodeSelector: + tolerations: + affinity: + backend: eggroll + enabledNN: false + logLevel: INFO + # subPath: "" + existingClaim: "" + claimName: python-data + storageClass: "python" + accessMode: ReadWriteOnce + size: 1Gi + clustermanager: + cores_per_node: 16 + nodes: 2 + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: fateflow + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: + rabbitmq: + host: rabbitmq + mng_port: 15672 + port: 5672 + user: fate + password: fate + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + client: + include: true + ip: client + type: ClusterIP + nodeSelector: + tolerations: + affinity: + subPath: "client" + existingClaim: "" + storageClass: "nodemanager-0" + accessMode: ReadWriteOnce + size: 1Gi + clustermanager: + include: true + ip: clustermanager + type: ClusterIP + nodeSelector: + tolerations: + affinity: + nodemanager: + include: true + list: + - name: nodemanager-0 + nodeSelector: + tolerations: + affinity: + sessionProcessorsPerNode: 2 + subPath: "nodemanager-0" + existingClaim: "" + storageClass: "nodemanager-0" + accessMode: ReadWriteOnce + size: 1Gi + - name: nodemanager-1 + nodeSelector: + tolerations: + affinity: + sessionProcessorsPerNode: 2 + subPath: "nodemanager-1" + existingClaim: "" + storageClass: "nodemanager-1" + accessMode: ReadWriteOnce + size: 1Gi + + client: + include: true + ip: client + type: ClusterIP + nodeSelector: + tolerations: + affinity: + subPath: "client" + existingClaim: "" + storageClass: "client" + accessMode: ReadWriteOnce + size: 1Gi + + mysql: + include: true + type: ClusterIP + nodeSelector: + tolerations: + affinity: + ip: mysql + port: 3306 + database: eggroll_meta + user: fate + password: fate_dev + subPath: "mysql" + existingClaim: "" + claimName: mysql-data + storageClass: "mysql" + accessMode: ReadWriteOnce + size: 1Gi + serving: + ip: 192.168.9.1 + port: 30095 + useRegistry: false + zookeeper: + hosts: + - serving-zookeeper.fate-serving-9999:2181 + use_acl: false + fateboard: + include: true + type: ClusterIP + username: admin + password: admin + + spark: + include: true + master: + Image: "" + ImageTag: "" + replicas: 1 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30977 + worker: + Image: "" + ImageTag: "" + replicas: 2 + resources: + requests: + cpu: "2" + memory: "4Gi" + limits: + cpu: "4" + memory: "8Gi" + nodeSelector: + tolerations: + affinity: + type: ClusterIP + hdfs: + include: true + namenode: + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30900 + datanode: + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nginx: + include: true + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30093 + grpcNodePort: 30098 + loadBalancerIP: + exchange: + ip: 192.168.10.1 + httpPort: 30003 + grpcPort: 30008 + route_table: +# 10000: +# proxy: +# - host: 192.168.10.1 +# http_port: 30103 +# grpc_port: 30108 +# fateflow: +# - host: 192.168.10.1 +# http_port: 30107 +# grpc_port: 30102 + rabbitmq: + include: true + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30094 + loadBalancerIP: + default_user: fate + default_pass: fate + user: fate + password: fate + route_table: +# 10000: +# host: 192.168.10.1 +# port: 30104 + + pulsar: + include: true + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30094 + httpsNodePort: 30099 + loadBalancerIP: + publicLB: + enabled: false + # exchange: + # ip: 192.168.10.1 + # port: 30000 + # domain: fate.org + route_table: +# 10000: +# host: 192.168.10.1 +# port: 30104 +# sslPort: 30109 +# proxy: "" +# + +# externalMysqlIp: mysql +# externalMysqlPort: 3306 +# externalMysqlDatabase: eggroll_meta +# externalMysqlUser: fate +# externalMysqlPassword: fate_dev`, + ValuesTemplate: `image: + registry: {{ .registry | default "federatedai" }} + isThridParty: {{ empty .registry | ternary "false" "true" }} + tag: {{ .imageTag | default "1.8.0-release" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +partyId: {{ .partyId | int64 | toString }} +partyName: {{ .name }} + +{{- $partyId := (.partyId | int64 | toString) }} + +{{- with .ingress }} +ingress: + {{- with .fateboard }} + fateboard: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .client }} + client: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .spark }} + spark: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .rabbitmq }} + rabbitmq: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .pulsar }} + pulsar: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + +{{- end }} + +{{- with .istio }} +istio: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +ingressClassName: {{ .ingressClassName | default "nginx"}} + +exchange: +{{- with .rollsite }} +{{- with .exchange }} + partyIp: {{ .ip }} + partyPort: {{ .port }} +{{- end }} +{{- end }} + +exchangeList: +{{- with .lbrollsite }} +{{- range .exchangeList }} + - id: {{ .id }} + ip: {{ .ip }} + port: {{ .port }} +{{- end }} +{{- end }} + +partyList: +{{- with .rollsite }} +{{- range .partyList }} + - partyId: {{ .partyId }} + partyIp: {{ .partyIp }} + partyPort: {{ .partyPort }} +{{- end }} +{{- end }} + +persistence: + enabled: {{ .persistence | default "false" }} + +modules: + rollsite: + include: {{ has "rollsite" .modules }} + {{- with .rollsite }} + ip: rollsite + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .polling }} + polling: + enabled: {{ .enabled }} + type: {{ .type }} + {{- with .server }} + server: + ip: {{ .ip }} + port: {{ .port }} + {{- end }} + {{- with .clientList }} + clientList: +{{ toYaml . | indent 6 }} + {{- end }} + concurrency: {{ .concurrency }} + {{- end }} + {{- end }} + + + lbrollsite: + include: {{ has "lbrollsite" .modules }} + {{- with .lbrollsite }} + ip: rollsite + type: {{ .type | default "ClusterIP" }} + loadBalancerIP: {{ .loadBalancerIP }} + nodePort: {{ .nodePort }} + size: {{ .size | default "2M" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + python: + include: {{ has "python" .modules }} + backend: {{ default "eggroll" .backend }} + {{- with .python }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + logLevel: {{ .logLevel }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + serviceAccountName: {{ .serviceAccountName }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + enabledNN: {{ .enabledNN | default false }} + existingClaim: {{ .existingClaim }} + claimName: {{ .claimName | default "python-data" }} + storageClass: {{ .storageClass | default "python" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .clustermanager }} + clustermanager: + cores_per_node: {{ .cores_per_node }} + nodes: {{ .nodes }} + {{- end }} + {{- with .spark }} + + spark: +{{ toYaml . | indent 6}} + {{- end }} + {{- with .hdfs }} + hdfs: + name_node: {{ .name_node }} + path_prefix: {{ .path_prefix }} + {{- end }} + {{- with .pulsar }} + pulsar: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + {{- end }} + {{- with .rabbitmq }} + rabbitmq: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + user: {{ .user }} + password: {{ .password }} + {{- end }} + {{- with .nginx }} + nginx: + host: {{ .host }} + http_port: {{ .http_port }} + grpc_port: {{ .grpc_port }} + {{- end }} + {{- end }} + + + clustermanager: + include: {{ has "clustermanager" .modules }} + {{- with .clustermanager }} + ip: clustermanager + type: "ClusterIP" + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + nodemanager: + include: {{ has "nodemanager" .modules }} + {{- with .nodemanager }} + list: + {{- $nodemanager := . }} + {{- range .count | int | until }} + - name: nodemanager-{{ . }} + {{- with $nodemanager.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end}} + {{- with $nodemanager.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end}} + {{- with $nodemanager.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end}} + sessionProcessorsPerNode: {{ $nodemanager.sessionProcessorsPerNode }} + subPath: "nodemanager-{{ . }}" + existingClaim: "" + storageClass: "{{ $nodemanager.storageClass }}" + accessMode: {{ $nodemanager.accessMode }} + size: {{ $nodemanager.size }} + {{- end }} + {{- end }} + + + client: + include: {{ has "client" .modules }} + {{- with .client }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "client" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + mysql: + include: {{ has "mysql" .modules }} + {{- with .mysql }} + type: {{ .type | default "ClusterIP" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + ip: {{ .ip | default "mysql" }} + port: {{ .port | default "3306" }} + database: {{ .database | default "eggroll_meta" }} + user: {{ .user | default "fate" }} + password: {{ .password | default "fate_dev" }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- end }} + + + serving: + ip: {{ .servingIp }} + port: {{ .servingPort }} + {{- with .serving }} + useRegistry: {{ .useRegistry | default false }} + zookeeper: +{{ toYaml .zookeeper | indent 6 }} + {{- end}} + + fateboard: + include: {{ has "fateboard" .modules }} + {{- with .fateboard }} + type: {{ .type }} + username: {{ .username }} + password: {{ .password }} + {{- end}} + + spark: + include: {{ has "spark" .modules }} + {{- with .spark }} + {{- if .master }} + master: + Image: "{{ .master.Image }}" + ImageTag: "{{ .master.ImageTag }}" + replicas: {{ .master.replicas }} + {{- with .master.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .master.type }} + {{- end }} + {{- if .worker }} + worker: + Image: "{{ .worker.Image }}" + ImageTag: "{{ .worker.ImageTag }}" + replicas: {{ .worker.replicas }} + {{- with .worker.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .worker.type | default "ClusterIP" }} + {{- end }} + {{- end }} + + + hdfs: + include: {{ has "hdfs" .modules }} + {{- with .hdfs }} + namenode: + {{- with .namenode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .namenode.type | default "ClusterIP" }} + nodePort: {{ .namenode.nodePort }} + existingClaim: {{ .namenode.existingClaim }} + storageClass: {{ .namenode.storageClass | default "" }} + accessMode: {{ .namenode.accessMode | default "ReadWriteOnce" }} + size: {{ .namenode.size }} + datanode: + {{- with .datanode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .datanode.type | default "ClusterIP" }} + existingClaim: {{ .datanode.existingClaim }} + storageClass: {{ .datanode.storageClass | default "" }} + accessMode: {{ .datanode.accessMode | default "ReadWriteOnce" }} + size: {{ .datanode.size }} + {{- end }} + + + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + httpPort: {{ .httpPort }} + grpcPort: {{ .grpcPort }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + + rabbitmq: + include: {{ has "rabbitmq" .modules }} + {{- with .rabbitmq }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + default_user: {{ .default_user }} + default_pass: {{ .default_pass }} + loadBalancerIP: {{ .loadBalancerIP }} + user: {{ .user }} + password: {{ .password }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + + pulsar: + include: {{ has "pulsar" .modules }} + {{- with .pulsar }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + httpsNodePort: {{ .httpsNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .publicLB}} + publicLB: + enabled: {{ .enabled | default false }} + {{- end }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + port: {{ .port }} + domain: {{ .domain | default "fate.org" }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size }} + {{- end }} + +externalMysqlIp: {{ .externalMysqlIp }} +externalMysqlPort: {{ .externalMysqlPort }} +externalMysqlDatabase: {{ .externalMysqlDatabase }} +externalMysqlUser: {{ .externalMysqlUser }} +externalMysqlPassword: {{ .externalMysqlPassword }}`, + ArchiveContent: nil, + Private: false, + }, + "3ce13cb2-5543-4b01-a5e4-9e4c4baa5973": { + Model: gorm.Model{ + ID: 3, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "3ce13cb2-5543-4b01-a5e4-9e4c4baa5973", + Name: "chart for FATE exchange v1.6.1 with fml-manager service v0.1.0", + Description: "This chart is for deploying FATE exchange v1.6.1 with fml-manager v0.1.0", + Type: entity.ChartTypeFATEExchange, + ChartName: "fate-exchange", + Version: "v1.6.1-fedlcm-v0.1.0", + AppVersion: "exchangev1.6.1 & fedlcmv0.1.0", + Chart: `apiVersion: v1 +appVersion: "exchangev1.6.1 & fedlcmv0.1.0" +description: A Helm chart for fate exchange and fml-manager +name: fate-exchange +version: v1.6.1-fedlcm-v0.1.0`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: fate-exchange +chartVersion: v1.6.1-fedlcm-v0.1.0 +partyId: 0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +# pullPolicy: +# persistence: false +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} +modules: + - trafficServer + - nginx + - postgres + - fmlManagerServer + +trafficServer: +{{- if .UseRegistry}} + image: trafficserver +{{- else }} + image: federatedai/trafficserver +{{- end }} + imageTag: latest + type: {{.ServiceType}} + route_table: + sni: + # replicas: 1 + # nodeSelector: + # tolerations: + # affinity: + # nodePort: + # loadBalancerIP: + +nginx: +{{- if .UseRegistry}} + image: nginx +{{- else }} + image: federatedai/nginx +{{- end }} + imageTag: 1.6.1-release + type: {{.ServiceType}} + route_table: + # replicas: 1 + # nodeSelector: + # tolerations: + # affinity: + # httpNodePort: + # grpcNodePort: + # loadBalancerIP: + +postgres: + image: postgres + imageTag: 13.3 + user: fml_manager + password: fml_manager + db: fml_manager + # nodeSelector: + # tolerations: + # affinity: + # subPath: "" + # existingClaim: "" + # storageClass: + # accessMode: ReadWriteOnce + # size: 1Gi + +fmlManagerServer: +{{- if .UseRegistry}} + image: fml-manager-server +{{- else }} + image: federatedai/fml-manager-server +{{- end }} + imageTag: v0.1.0 + type: {{.ServiceType}} + # nodeSelector: + # tolerations: + # affinity: + # nodePort: + # loadBalancerIP: + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: fml_manager + # postgresUser: fml_manager + # postgresPassword: fml_manager + # tlsPort: 8443 + # serverCert: /var/lib/fml_manager/cert/server.crt + # serverKey: /var/lib/fml_manager/cert/server.key + # clientCert: /var/lib/fml_manager/cert/client.crt + # clientKey: /var/lib/fml_manager/cert/client.key + # caCert: /var/lib/fml_manager/cert/ca.crt + # tlsEnabled: 'true'`, + Values: `partyId: 1 +partyName: fate-exchange + +image: + registry: + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +persistence: + enabled: false + +modules: + nginx: + include: true + image: federatedai/nginx + imageTag: 1.6.1-release + type: ClusterIP + # httpNodePort: 30003 + # grpcNodePort: 30008 + # loadBalancerIP: + # nodeSelector: + # tolerations: + # affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + # 10000: + # fateflow: + # - grpc_port: 30102 + # host: 192.168.10.1 + # http_port: 30107 + # proxy: + # - grpc_port: 30108 + # host: 192.168.10.1 + # http_port: 30103 + # 9999: + # fateflow: + # - grpc_port: 30092 + # host: 192.168.9.1 + # http_port: 30097 + # proxy: + # - grpc_port: 30098 + # host: 192.168.9.1 + # http_port: 30093 + trafficServer: + image: federatedai/trafficserver + imageTag: latest + include: true + type: ClusterIP + # nodePort: 30007 + # loadBalancerIP: + # nodeSelector: + # tolerations: + # affinity: + # route_table is used to configure the cluster information of all parties that join in the exchange deployment mode. (When Spark was used as the calculation engine at the time) + route_table: + sni: + # - fqdn: 10000.fate.org + # tunnelRoute: 192.168.0.2:30109 + # - fqdn: 9999.fate.org + # tunnelRoute: 192.168.0.3:30099 + postgres: + include: true + type: ClusterIP + image: postgres + imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + user: fml_manager + password: fml_manager + db: fml_manager + # subPath: "" + # existingClaim: "" + # storageClass: "" + # accessMode: ReadWriteOnce + # size: 1Gi + + fmlManagerServer: + include: true + image: federatedai/fml-manager-server + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: 192.168.0.1 + postgresHost: postgres + postgresPort: 5432 + postgresDb: fml_manager + postgresUser: fml_manager + postgresPassword: fml_manager + tlsPort: 8443 + serverCert: /var/lib/fml_manager/cert/server.crt + serverKey: /var/lib/fml_manager/cert/server.key + clientCert: /var/lib/fml_manager/cert/client.crt + clientKey: /var/lib/fml_manager/cert/client.key + caCert: /var/lib/fml_manager/cert/ca.crt + tlsEnabled: 'true'`, + ValuesTemplate: `partyId: {{ .partyId }} +partyName: {{ .name }} + +image: + registry: {{ .registry | default "" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + + +modules: + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + replicas: {{ .replicas }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + trafficServer: + include: {{ has "trafficServer" .modules }} + {{- with .trafficServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + replicas: {{ .replicas }} + nodePort: {{ .nodePort }} + route_table: + sni: + {{- range .route_table.sni }} + - fqdn: {{ .fqdn }} + tunnelRoute: {{ .tunnelRoute }} + {{- end }} + {{- end }} + + postgres: + include: {{ has "postgres" .modules }} + {{- with .postgres }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type | default "ClusterIP" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + user: {{ .user }} + password: {{ .password }} + db: {{ .db }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode }} + size: {{ .size }} + {{- end }} + + fmlManagerServer: + include: {{ has "fmlManagerServer" .modules }} + {{- with .fmlManagerServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + postgresHost: {{ .postgresHost | default "postgres" }} + postgresPort: {{ .postgresPort | default "5432" }} + postgresDb: {{ .postgresDb | default "fml_manager" }} + postgresUser: {{ .postgresUser | default "fml_manager" }} + postgresPassword: {{ .postgresPassword | default "fml_manager" }} + tlsPort: {{ .tlsPort | default "8443" }} + serverCert: {{ .serverCert | default "/var/lib/fml_manager/cert/server.crt" }} + serverKey: {{ .serverKey | default "/var/lib/fml_manager/cert/server.key" }} + clientCert: {{ .clientCert| default "/var/lib/fml_manager/cert/client.crt" }} + clientKey: {{ .clientKey | default "/var/lib/fml_manager/cert/client.key" }} + caCert: {{ .caCert | default "/var/lib/fml_manager/cert/ca.crt" }} + tlsEnabled: {{ .tlsEnabled | default "true" }} + {{- end }}`, + ArchiveContent: mock.FATEExchange161WithManagerChartArchiveContent, + Private: true, + }, + "09356325-b1d9-4598-b952-1b2bb73baf51": { + Model: gorm.Model{ + ID: 4, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "09356325-b1d9-4598-b952-1b2bb73baf51", + Name: "chart for FATE cluster v1.6.1 with site-portal v0.1.0", + Description: "This is chart for installing FATE cluster v1.6.1 with site-portal v0.1.0", + Type: entity.ChartTypeFATECluster, + ChartName: "fate", + Version: "v1.6.1-fedlcm-v0.1.0", + AppVersion: "fatev1.6.1+fedlcmv0.1.0", + ArchiveContent: mock.FATE161WithPortalChartArchiveContent, + Chart: `apiVersion: v1 +appVersion: "fatev1.6.1+fedlcmv0.1.0" +description: Helm chart for FATE and site-portal in FedLCM +name: fate +version: v1.6.1-fedlcm-v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git + - https://github.com/FederatedAI/FATE.git`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: fate +chartVersion: v1.6.1-fedlcm-v0.1.0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +partyId: {{.PartyID}} +persistence: {{.EnablePersistence}} +# pullPolicy: IfNotPresent +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} + +modules: + - mysql + - python + - fateboard + - client + - spark + - hdfs + - nginx + - pulsar + - frontend + - sitePortalServer + - postgres + +backend: spark + +ingress: + fateboard: + hosts: + - name: {{.Name}}.fateboard.{{.Domain}} + client: + hosts: + - name: {{.Name}}.notebook.{{.Domain}} + spark: + hosts: + - name: {{.Name}}.spark.{{.Domain}} + pulsar: + hosts: + - name: {{.Name}}.pulsar.{{.Domain}} + +python: +{{- if .UseRegistry}} + image: python-spark +{{- else }} + image: federatedai/python-spark +{{- end }} + imageTag: 1.6.1-release + existingClaim: "" + storageClass: {{ .StorageClass }} + accessMode: ReadWriteOnce + size: 10Gi + # type: ClusterIP + # httpNodePort: + # grpcNodePort: + # resources: + # nodeSelector: + # tolerations: + # affinity: + # enabledNN: true + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + +fateboard: +{{- if .UseRegistry}} + image: fateboard +{{- else }} + image: federatedai/fateboard +{{- end }} + imageTag: 1.6.1-release + type: ClusterIP + username: admin + password: admin + +client: +{{- if .UseRegistry}} + image: client +{{- else }} + image: federatedai/client +{{- end }} + imageTag: 1.6.1-release + subPath: "client" + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + +mysql: + image: mysql + imageTag: 8 + subPath: "mysql" + size: 1Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + # nodeSelector: + # tolerations: + # affinity: + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + +spark: + master: +{{- if .UseRegistry}} + image: "spark-master" +{{- else }} + image: "federatedai/spark-master" +{{- end }} + imageTag: "1.6.1-release" + replicas: 1 + # resources: + # requests: + # cpu: "1" + # memory: "2Gi" + # limits: + # cpu: "1" + # memory: "2Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + worker: +{{- if .UseRegistry}} + image: "spark-worker" +{{- else }} + image: "federatedai/spark-worker" +{{- end }} + imageTag: "1.6.1-release" + replicas: 2 + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + +hdfs: + namenode: +{{- if .UseRegistry}} + image: hadoop-namenode +{{- else }} + image: federatedai/hadoop-namenode +{{- end }} + imageTag: 2.0.0-hadoop2.7.4-java8 + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + datanode: +{{- if .UseRegistry}} + image: hadoop-datanode +{{- else }} + image: federatedai/hadoop-datanode +{{- end }} + imageTag: 2.0.0-hadoop2.7.4-java8 + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + +nginx: + {{- if .UseRegistry}} + image: nginx + {{- else }} + image: federatedai/nginx + {{- end }} + imageTag: 1.6.1-release + type: {{.ServiceType}} + exchange: + ip: {{.ExchangeNginxHost}} + httpPort: {{.ExchangeNginxPort}} + # nodeSelector: + # tolerations: + # affinity: + # loadBalancerIP: + # httpNodePort: + # grpcNodePort: + +pulsar: + {{- if .UseRegistry}} + image: pulsar + {{- else }} + image: federatedai/pulsar + {{- end }} + imageTag: 2.7.0 + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + publicLB: + enabled: true + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # httpNodePort: + # httpsNodePort: + # loadBalancerIP: + +postgres: + image: postgres + imageTag: 13.3 + user: site_portal + password: site_portal + db: site_portal + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # type: ClusterIP + # nodeSelector: + # tolerations: + # affinity: + # user: site_portal + # password: site_portal + # db: site_portal + # subPath: "" + +frontend: + type: {{.ServiceType}} + {{- if .UseRegistry}} + image: site-portal-frontend + {{- else }} + image: federatedai/site-portal-frontend + {{- end }} + imageTag: v0.1.0 + type: {{.ServiceType}} + # nodeSelector: + # tolerations: + # affinity: + # nodePort: + # loadBalancerIP: + +sitePortalServer: + {{- if .UseRegistry}} + image: site-portal-server + {{- else }} + image: federatedai/site-portal-server + {{- end }} + imageTag: v0.1.0 + existingClaim: "" + storageClass: {{ .StorageClass }} + accessMode: ReadWriteOnce + size: 1Gi + # type: ClusterIP + # nodeSelector: + # tolerations: + # affinity: + # postgresHost: postgres + # postgresPort: 5432 + # postgresDb: site_portal + # postgresUser: site_portal + # postgresPassword: site_portal + # adminPassword: admin + # userPassword: user + # serverCert: /var/lib/site-portal/cert/server.crt + # serverKey: /var/lib/site-portal/cert/server.key + # clientCert: /var/lib/site-portal/cert/client.crt + # clientKey: /var/lib/site-portal/cert/client.key + # caCert: /var/lib/site-portal/cert/ca.crt + # tlsEnabled: 'true' + # tlsPort: 8443 + tlsCommonName: {{.SitePortalTLSCommonName}} +`, + Values: ` +image: + registry: + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +partyId: 9999 +partyName: fate-9999 + +istio: + enabled: false + +podSecurityPolicy: + enabled: false + +ingress: + fateboard: + # annotations: + hosts: + - name: fateboard.kubefate.net + path: / + tls: [] + # - secretName: my-tls-secret + # hosts: + # - fateboard.kubefate.net + client: + # annotations: + hosts: + - name: notebook.kubefate.net + path: / + tls: [] + spark: + # annotations: + hosts: + - name: spark.kubefate.net + path: / + tls: [] + pulsar: + # annotations: + hosts: + - name: pulsar.kubefate.net + path: / + tls: [] + frontend: + # annotations: + hosts: + - name: frontend.example.com + path: / + tls: [] + +persistence: + enabled: false + +modules: + python: + image: federatedai/python-spark + imageTag: 1.6.1-release + include: true + type: ClusterIP + httpNodePort: 30097 + grpcNodePort: 30092 + loadBalancerIP: + serviceAccountName: + nodeSelector: + tolerations: + affinity: + backend: eggroll + enabledNN: false + # subPath: "" + existingClaim: "" + claimName: python-data + storageClass: "python" + accessMode: ReadWriteOnce + size: 1Gi + clustermanager: + cores_per_node: 16 + nodes: 2 + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: fateflow + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: + rabbitmq: + host: rabbitmq + mng_port: 15672 + port: 5672 + user: fate + password: fate + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + + fateboard: + include: true + image: federatedai/fateboard + imageTag: 1.6.1-release + type: ClusterIP + username: admin + password: admin + + client: + include: true + image: federatedai/client + imageTag: 1.6.1-release + ip: client + type: ClusterIP + nodeSelector: + tolerations: + affinity: + subPath: "client" + existingClaim: "" + storageClass: "client" + accessMode: ReadWriteOnce + size: 1Gi + + mysql: + include: true + image: mysql + imageTag: 8 + type: ClusterIP + nodeSelector: + tolerations: + affinity: + ip: mysql + port: 3306 + database: eggroll_meta + user: fate + password: fate_dev + subPath: "mysql" + existingClaim: "" + claimName: mysql-data + storageClass: "mysql" + accessMode: ReadWriteOnce + size: 1Gi + + serving: + ip: 192.168.9.1 + port: 30095 + useRegistry: false + zookeeper: + hosts: + - serving-zookeeper.fate-serving-9999:2181 + use_acl: false + + spark: + include: true + master: + image: "federatedai/spark-master" + imageTag: "1.6.1-release" + replicas: 1 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30977 + worker: + image: "federatedai/spark-worker" + imageTag: "1.6.1-release" + replicas: 2 + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + nodeSelector: + tolerations: + affinity: + type: ClusterIP + hdfs: + include: true + namenode: + image: federatedai/hadoop-namenode + imageTag: 2.0.0-hadoop2.7.4-java8 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30900 + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: hdfs + datanode: + image: federatedai/hadoop-datanode + imageTag: 2.0.0-hadoop2.7.4-java8 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: hdfs + nginx: + include: true + image: federatedai/nginx + imageTag: 1.6.1-release + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30093 + grpcNodePort: 30098 + loadBalancerIP: + exchange: + ip: 192.168.10.1 + httpPort: 30003 + grpcPort: 30008 + route_table: +# 10000: +# proxy: +# - host: 192.168.10.1 +# http_port: 30103 +# grpc_port: 30108 +# fateflow: +# - host: 192.168.10.1 +# http_port: 30107 +# grpc_port: 30102 + + pulsar: + include: true + image: federatedai/pulsar + imageTag: 2.7.0 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + httpNodePort: 30094 + httpsNodePort: 30099 + loadBalancerIP: + publicLB: + enabled: false + existingClaim: "" + storageClass: "pulsar" + accessMode: ReadWriteOnce + size: 1Gi + # exchange: + # ip: 192.168.10.1 + # port: 30000 + # domain: fate.org + route_table: +# 10000: +# host: 192.168.10.1 +# port: 30104 +# sslPort: 30109 +# proxy: "" +# + + postgres: + include: true + image: postgres + imageTag: 13.3 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: + user: site_portal + password: site_portal + db: site_portal + # subPath: "" + existingClaim: "" + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi + + frontend: + include: true + image: federatedai/site-portal-frontend + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + + # nodePort: + # loadBalancerIP: + + sitePortalServer: + include: true + image: federatedai/site-portal-server + imageTag: v0.1.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + # nodePort: + # loadBalancerIP: + existingClaim: "" + storageClass: "sitePortalServer" + accessMode: ReadWriteOnce + size: 1Gi + postgresHost: postgres + postgresPort: 5432 + postgresDb: site_portal + postgresUser: site_portal + postgresPassword: site_portal + adminPassword: admin + userPassword: user + serverCert: /var/lib/site-portal/cert/server.crt + serverKey: /var/lib/site-portal/cert/server.key + clientCert: /var/lib/site-portal/cert/client.crt + clientKey: /var/lib/site-portal/cert/client.key + caCert: /var/lib/site-portal/cert/ca.crt + tlsEnabled: 'true' + tlsPort: 8443 + tlsCommonName: site-1.server.example.com + + +# externalMysqlIp: mysql +# externalMysqlPort: 3306 +# externalMysqlDatabase: eggroll_meta +# externalMysqlUser: fate +# externalMysqlPassword: fate_dev`, + ValuesTemplate: ` +image: + registry: {{ .registry | default "" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +partyId: {{ .partyId | int64 | toString }} +partyName: {{ .name }} + +{{- with .istio }} +istio: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .ingress }} +ingress: + {{- with .fateboard }} + fateboard: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .client }} + client: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .spark }} + spark: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- with .pulsar }} + pulsar: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + {{- if not .tlsEnabled}} + {{- with .frontend }} + frontend: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +persistence: + enabled: {{ .persistence | default "false" }} + +modules: + + + python: + include: {{ has "python" .modules }} + backend: {{ default "spark" .backend }} + {{- with .python }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + serviceAccountName: {{ .serviceAccountName }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + enabledNN: {{ .enabledNN | default false }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "python" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .clustermanager }} + clustermanager: + cores_per_node: {{ .cores_per_node }} + nodes: {{ .nodes }} + {{- end }} + {{- with .spark }} + + spark: +{{ toYaml . | indent 6}} + {{- end }} + {{- with .hdfs }} + hdfs: + name_node: {{ .name_node }} + path_prefix: {{ .path_prefix }} + {{- end }} + {{- with .pulsar }} + pulsar: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + {{- end }} + {{- with .rabbitmq }} + rabbitmq: + host: {{ .host }} + mng_port: {{ .mng_port }} + port: {{ .port }} + user: {{ .user }} + password: {{ .password }} + {{- end }} + {{- with .nginx }} + nginx: + host: {{ .host }} + http_port: {{ .http_port }} + grpc_port: {{ .grpc_port }} + {{- end }} + {{- end }} + + + fateboard: + include: {{ has "fateboard" .modules }} + {{- with .fateboard }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type }} + username: {{ .username }} + password: {{ .password }} + {{- end}} + + + client: + include: {{ has "client" .modules }} + {{- with .client }} + image: {{ .image }} + imageTag: {{ .imageTag }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "client" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + mysql: + include: {{ has "mysql" .modules }} + {{- with .mysql }} + image: {{ .image }} + imageTag: {{ .imageTag }} + type: {{ .type | default "ClusterIP" }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + ip: {{ .ip | default "mysql" }} + port: {{ .port | default "3306" }} + database: {{ .database | default "eggroll_meta" }} + user: {{ .user | default "fate" }} + password: {{ .password | default "fate_dev" }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- end }} + + serving: + ip: {{ .servingIp }} + port: {{ .servingPort }} + {{- with .serving }} + useRegistry: {{ .useRegistry | default false }} + zookeeper: +{{ toYaml .zookeeper | indent 6 }} + {{- end}} + + spark: + include: {{ has "spark" .modules }} + {{- with .spark }} + {{- if .master }} + master: + image: "{{ .master.image }}" + imageTag: "{{ .master.imageTag }}" + replicas: {{ .master.replicas }} + {{- with .master.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .master.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .master.type | default "ClusterIP" }} + {{- end }} + {{- if .worker }} + worker: + image: "{{ .worker.image }}" + imageTag: "{{ .worker.imageTag }}" + replicas: {{ .worker.replicas }} + {{- with .worker.resources }} + resources: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .worker.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .worker.type | default "ClusterIP" }} + {{- end }} + {{- end }} + + + hdfs: + include: {{ has "hdfs" .modules }} + {{- with .hdfs }} + namenode: + image: {{ .namenode.image }} + imageTag: {{ .namenode.imageTag }} + {{- with .namenode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .namenode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .namenode.type | default "ClusterIP" }} + nodePort: {{ .namenode.nodePort }} + existingClaim: {{ .namenode.existingClaim }} + storageClass: {{ .namenode.storageClass | default "" }} + accessMode: {{ .namenode.accessMode | default "ReadWriteOnce" }} + size: {{ .namenode.size | default "1Gi" }} + datanode: + image: {{ .datanode.image }} + imageTag: {{ .datanode.imageTag }} + {{- with .datanode.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .datanode.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + type: {{ .datanode.type | default "ClusterIP" }} + existingClaim: {{ .datanode.existingClaim }} + storageClass: {{ .datanode.storageClass | default "" }} + accessMode: {{ .datanode.accessMode | default "ReadWriteOnce" }} + size: {{ .datanode.size | default "1Gi" }} + {{- end }} + + + nginx: + include: {{ has "nginx" .modules }} + {{- with .nginx }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + httpPort: {{ .httpPort }} + grpcPort: {{ .grpcPort }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + {{- end }} + + + pulsar: + include: {{ has "pulsar" .modules }} + {{- with .pulsar }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + httpsNodePort: {{ .httpsNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- with .publicLB}} + publicLB: + enabled: {{ .enabled | default false }} + {{- end }} + {{- with .exchange }} + exchange: + ip: {{ .ip }} + port: {{ .port }} + domain: {{ .domain | default "fate.org" }} + {{- end }} + route_table: + {{- range $key, $val := .route_table }} + {{ $key }}: +{{ toYaml $val | indent 8 }} + {{- end }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass | default "" }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + {{- end }} + + + postgres: + include: {{ has "postgres" .modules }} + {{- with .postgres }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + user: {{ .user }} + password: {{ .password }} + db: {{ .db }} + subPath: {{ .subPath }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode }} + size: {{ .size }} + {{- end }} + frontend: + include: {{ has "frontend" .modules }} + {{- with .frontend }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }} + + sitePortalServer: + include: {{ has "sitePortalServer" .modules }} + {{- with .sitePortalServer }} + image: {{ .image }} + imageTag: {{ .imageTag }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "ClusterIP" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + existingClaim: {{ .existingClaim }} + storageClass: {{ .storageClass }} + accessMode: {{ .accessMode | default "ReadWriteOnce" }} + size: {{ .size | default "1Gi" }} + postgresHost: {{ .postgresHost | default "postgres" }} + postgresPort: {{ .postgresPort | default "5432" }} + postgresDb: {{ .postgresDb | default "site_portal" }} + postgresUser: {{ .postgresUser | default "site_portal" }} + postgresPassword: {{ .postgresPassword | default "site_portal" }} + adminPassword: {{ .adminPassword | default "admin" }} + userPassword: {{ .userPassword | default "user" }} + serverCert: {{ .serverCert| default "/var/lib/site-portal/cert/server.crt" }} + serverKey: {{ .serverKey | default "/var/lib/site-portal/cert/server.key" }} + clientCert: {{ .clientCert | default "/var/lib/site-portal/cert/client.crt" }} + clientKey: {{ .clientKey | default "/var/lib/site-portal/cert/client.key" }} + caCert: {{ .caCert | default "/var/lib/site-portal/cert/ca.crt" }} + tlsEnabled: {{ .tlsEnabled | default "'true'" }} + tlsPort: {{ .tlsPort | default "8443" }} + tlsCommonName: {{ .tlsCommonName | default "site-1.server.example.com" }} + {{- end }} + + +externalMysqlIp: {{ .externalMysqlIp }} +externalMysqlPort: {{ .externalMysqlPort }} +externalMysqlDatabase: {{ .externalMysqlDatabase }} +externalMysqlUser: {{ .externalMysqlUser }} +externalMysqlPassword: {{ .externalMysqlPassword }} +`, + Private: true, + }, + "516a10e2-0b96-417c-812b-ee45ed197e81": { + Model: gorm.Model{ + ID: 101, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "516a10e2-0b96-417c-812b-ee45ed197e81", + Name: "chart for FedLCM private OpenFL Director v0.1.0", + Description: "This chart is for deploying FedLCM's OpenFL director v0.1.0 services, based from OpenFL 1.3 release", + Type: entity.ChartTypeOpenFLDirector, + ChartName: "openfl-director", + Version: "v0.1.0", + AppVersion: "openfl-director-v0.1.0", + Chart: `apiVersion: v1 +appVersion: "openfl-director-v0.1.0" +description: A Helm chart for openfl director, based on official OpenFL container image +name: openfl-director +version: v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: openfl-director +chartVersion: v0.1.0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +# pullPolicy: +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} +modules: + - director + - notebook + +ingress: + notebook: + # annotations: + hosts: + - name: {{.Name}}.notebook.{{.Domain}} + path: / + # tls: + +director: + image: fedlcm-openfl + imageTag: v0.1.0 + type: {{.ServiceType}} + sampleShape: "{{.SampleShape}}" + targetShape: "{{.TargetShape}}" +# envoyHealthCheckPeriod: 60 +# nodeSelector: +# tolerations: +# affinity: +# nodePort: +# loadBalancerIp: + +notebook: + image: fedlcm-openfl + imageTag: v0.1.0 + type: {{.ServiceType}} + password: {{.JupyterPassword}} +# nodeSelector: +# tolerations: +# affinity: +# nodePort: +# loadBalancerIp:`, + Values: `image: + registry: federatedai + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +ingress: + notebook: + # annotations: + hosts: + - name: notebook.openfl.example.com + path: / + tls: [] + +modules: + director: + image: fedlcm-openfl + imageTag: v0.1.0 + sampleShape: "['1']" + targetShape: "['1']" + envoyHealthCheckPeriod: 60 + # nodeSelector: + # tolerations: + # affinity: + type: NodePort + # nodePort: + # loadBalancerIP: + + notebook: + image: fedlcm-openfl + imageTag: v0.1.0 + # password: + # nodeSelector: + # tolerations: + # affinity: + type: NodePort + # nodePort: + # loadBalancerIP:`, + ValuesTemplate: `name: {{ .name }} + +image: + registry: {{ .registry | default "federatedai" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +{{- with .ingress }} +ingress: + {{- with .notebook }} + notebook: + {{- with .annotations }} + annotations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .hosts }} + hosts: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tls }} + tls: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} +{{- end }} + +modules: + director: + {{- with .director }} + image: {{ .image }} + sampleShape: "{{ .sampleShape }}" + targetShape: "{{ .targetShape }}" + envoyHealthCheckPeriod: {{ .envoyHealthCheckPeriod | default "60"}} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "NodePort" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }} + + notebook: + {{- with .notebook}} + image: {{ .image }} + password: {{ .password }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + type: {{ .type | default "NodePort" }} + nodePort: {{ .nodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + {{- end }}`, + ArchiveContent: mock.FedLCMOpenFLDirector010ChartArchiveContent, + Private: true, + }, + "c62b27a6-bf0f-4515-840a-2554ed63aa56": { + Model: gorm.Model{ + ID: 102, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "c62b27a6-bf0f-4515-840a-2554ed63aa56", + Name: "chart for FedLCM private OpenFL Envoy v0.1.0", + Description: "This chart is for deploying OpenFL envoy built for FedLCM, based from OpenFL 1.3 release", + Type: entity.ChartTypeOpenFLEnvoy, + ChartName: "openfl-envoy", + Version: "v0.1.0", + AppVersion: "openfl-envoy-v0.1.0", + Chart: `apiVersion: v1 +appVersion: "openfl-envoy-v0.1.0" +description: A Helm chart for openfl envoy +name: openfl-envoy +version: v0.1.0 +sources: + - https://github.com/FederatedAI/KubeFATE.git`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: openfl-envoy +chartVersion: v0.1.0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} +modules: + - envoy + +envoy: + image: fedlcm-openfl + imageTag: v0.1.0 + directorFqdn: {{.DirectorFQDN}} + directorIp: {{.DirectorIP}} + directorPort: {{.DirectorPort}} + aggPort: {{.AggPort}} + envoyConfigs: +{{ .EnvoyConfig | trimAll "\n" | indent 4 }} +# nodeSelector: +# tolerations: +# affinity:`, + Values: `name: envoy-1 + +image: + registry: federatedai + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +modules: + envoy: + image: fedlcm-openfl + imageTag: v0.1.0 + directorFqdn: director + directorIp: 192.168.1.1 + directorPort: 50051 + aggPort: 50052 + envoyConfigs: + # nodeSelector: + # tolerations: + # affinity:`, + ValuesTemplate: `name: {{ .name }} + +image: + registry: {{ .registry | default "federatedai" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +{{- with .podSecurityPolicy }} +podSecurityPolicy: + enabled: {{ .enabled | default false }} +{{- end }} + +modules: + envoy: + {{- with .envoy }} + image: {{ .image }} + imageTag: {{ .imageTag }} + directorFqdn: {{ .directorFqdn }} + directorIp: {{ .directorIp }} + directorPort: {{ .directorPort }} + aggPort: {{ .aggPort }} + envoyConfigs: +{{ toYaml .envoyConfigs | indent 6 }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }}`, + ArchiveContent: mock.FedLCMOpenFLEnvoy010ChartArchiveContent, + Private: true, + }, + } +) diff --git a/server/infrastructure/gorm/database.go b/server/infrastructure/gorm/database.go new file mode 100644 index 00000000..6feb5830 --- /dev/null +++ b/server/infrastructure/gorm/database.go @@ -0,0 +1,73 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/spf13/viper" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +const ( + dsnTemplate = "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=Asia/Shanghai" +) + +// InitDB initialize the connection to the PostgresSql db +func InitDB() error { + host := viper.GetString("postgres.host") + port := viper.GetString("postgres.port") + dbName := viper.GetString("postgres.db") + user := viper.GetString("postgres.user") + password := viper.GetString("postgres.password") + if host == "" || port == "" || dbName == "" || user == "" || password == "" { + panic("database information incomplete") + } + + sslMode := viper.GetString("postgres.sslmode") + if sslMode == "" { + sslMode = "disable" + } + + debugLog, _ := strconv.ParseBool(viper.GetString("postgres.debug")) + loglevel := logger.Silent + if debugLog { + loglevel = logger.Info + } + + dsn := fmt.Sprintf(dsnTemplate, host, port, user, password, dbName, sslMode) + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: loglevel, + Colorful: false, + }, + ) + if _db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: newLogger}); err != nil { + return err + } else { + db = _db + } + return nil +} diff --git a/server/infrastructure/gorm/endpoint_kubefate_repo.go b/server/infrastructure/gorm/endpoint_kubefate_repo.go new file mode 100644 index 00000000..5d447e00 --- /dev/null +++ b/server/infrastructure/gorm/endpoint_kubefate_repo.go @@ -0,0 +1,93 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// EndpointKubeFATERepo is the implementation of the repo.EndpointRepository interface +type EndpointKubeFATERepo struct{} + +var _ repo.EndpointRepository = (*EndpointKubeFATERepo)(nil) + +// ErrEndpointExist means new endpoint cannot be created due to the existence of the same-name endpoint +var ErrEndpointExist = errors.New("endpoint already exists") + +func (r *EndpointKubeFATERepo) Create(instance interface{}) error { + endpoint := instance.(*entity.EndpointKubeFATE) + + var count int64 + db.Model(&entity.EndpointKubeFATE{}).Where("name = ?", endpoint.Name).Count(&count) + if count > 0 { + return ErrEndpointExist + } + + if err := db.Create(endpoint).Error; err != nil { + return err + } + return nil +} + +func (r *EndpointKubeFATERepo) List() (interface{}, error) { + var endpoints []entity.EndpointKubeFATE + if err := db.Find(&endpoints).Error; err != nil { + return nil, err + } + return endpoints, nil +} + +func (r *EndpointKubeFATERepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.EndpointKubeFATE{}).Error +} + +func (r *EndpointKubeFATERepo) GetByUUID(uuid string) (interface{}, error) { + endpoint := &entity.EndpointKubeFATE{} + if err := db.Where("uuid = ?", uuid).First(endpoint).Error; err != nil { + return nil, err + } + return endpoint, nil +} + +func (r *EndpointKubeFATERepo) ListByInfraProviderUUID(infraUUID string) (interface{}, error) { + var endpoints []entity.EndpointKubeFATE + err := db.Where("infra_provider_uuid = ?", infraUUID).Find(&endpoints).Error + if err != nil { + return 0, err + } + return endpoints, nil +} + +func (r *EndpointKubeFATERepo) UpdateStatusByUUID(instance interface{}) error { + endpoint := instance.(*entity.EndpointKubeFATE) + return db.Model(&entity.EndpointKubeFATE{}).Where("uuid = ?", endpoint.UUID). + Update("status", endpoint.Status).Error +} + +func (r *EndpointKubeFATERepo) UpdateInfoByUUID(instance interface{}) error { + endpoint := instance.(*entity.EndpointKubeFATE) + return db.Where("uuid = ?", endpoint.UUID). + Select("name", "description", "version", "config", "status"). + Updates(endpoint).Error +} + +// InitTable makes sure the table is created in the db +func (r *EndpointKubeFATERepo) InitTable() { + if err := db.AutoMigrate(entity.EndpointKubeFATE{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/event_repo.go b/server/infrastructure/gorm/event_repo.go new file mode 100644 index 00000000..958fa65b --- /dev/null +++ b/server/infrastructure/gorm/event_repo.go @@ -0,0 +1,50 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// EventRepo is the implementation of the repo.EventRepository interface +type EventRepo struct{} + +var _ repo.EventRepository = (*EventRepo)(nil) + +func (r *EventRepo) ListByEntityUUID(EntityUUID string) (interface{}, error) { + var eventList []entity.Event + err := db.Where("entity_uuid = ?", EntityUUID).Order("created_at desc").Find(&eventList).Error + if err != nil { + return 0, err + } + return eventList, nil +} + +func (r *EventRepo) Create(instance interface{}) error { + event := instance.(*entity.Event) + + if err := db.Create(event).Error; err != nil { + return err + } + return nil +} + +// InitTable makes sure the table is created in the db +func (r *EventRepo) InitTable() { + if err := db.AutoMigrate(entity.Event{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/federation_fate_repo.go b/server/infrastructure/gorm/federation_fate_repo.go new file mode 100644 index 00000000..fa1767aa --- /dev/null +++ b/server/infrastructure/gorm/federation_fate_repo.go @@ -0,0 +1,70 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// FederationFATERepo implements the repo.FederationRepository interface +type FederationFATERepo struct{} + +var _ repo.FederationRepository = (*FederationFATERepo)(nil) + +// ErrFederationExist means new federation cannot be created due to the existence of the same-name federation +var ErrFederationExist = errors.New("federation already exists") + +func (r *FederationFATERepo) Create(instance interface{}) error { + var count int64 + federation := instance.(*entity.FederationFATE) + db.Model(&entity.FederationFATE{}).Where("name = ?", federation.Name).Count(&count) + if count > 0 { + return ErrFederationExist + } + + if err := db.Create(federation).Error; err != nil { + return err + } + return nil +} + +func (r *FederationFATERepo) List() (interface{}, error) { + var federationList []entity.FederationFATE + if err := db.Find(&federationList).Error; err != nil { + return nil, err + } + return federationList, nil +} + +func (r *FederationFATERepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.FederationFATE{}).Error +} + +func (r *FederationFATERepo) GetByUUID(uuid string) (interface{}, error) { + federation := &entity.FederationFATE{} + if err := db.Where("uuid = ?", uuid).First(federation).Error; err != nil { + return nil, err + } + return federation, nil +} + +// InitTable makes sure the table is created in the db +func (r *FederationFATERepo) InitTable() { + if err := db.AutoMigrate(entity.FederationFATE{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/federation_openfl_repo.go b/server/infrastructure/gorm/federation_openfl_repo.go new file mode 100644 index 00000000..915050a6 --- /dev/null +++ b/server/infrastructure/gorm/federation_openfl_repo.go @@ -0,0 +1,66 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// FederationOpenFLRepo implements the repo.FederationRepository interface +type FederationOpenFLRepo struct{} + +var _ repo.FederationRepository = (*FederationOpenFLRepo)(nil) + +func (r *FederationOpenFLRepo) Create(instance interface{}) error { + var count int64 + federation := instance.(*entity.FederationOpenFL) + db.Model(&entity.FederationOpenFL{}).Where("name = ?", federation.Name).Count(&count) + if count > 0 { + return ErrFederationExist + } + + if err := db.Create(federation).Error; err != nil { + return err + } + return nil +} + +func (r *FederationOpenFLRepo) List() (interface{}, error) { + var federationList []entity.FederationOpenFL + if err := db.Find(&federationList).Error; err != nil { + return nil, err + } + return federationList, nil +} + +func (r *FederationOpenFLRepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.FederationOpenFL{}).Error +} + +func (r *FederationOpenFLRepo) GetByUUID(uuid string) (interface{}, error) { + federation := &entity.FederationOpenFL{} + if err := db.Where("uuid = ?", uuid).First(federation).Error; err != nil { + return nil, err + } + return federation, nil +} + +// InitTable makes sure the table is created in the db +func (r *FederationOpenFLRepo) InitTable() { + if err := db.AutoMigrate(entity.FederationOpenFL{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/infra_provider_kubernetes_repo.go b/server/infrastructure/gorm/infra_provider_kubernetes_repo.go new file mode 100644 index 00000000..c25855f3 --- /dev/null +++ b/server/infrastructure/gorm/infra_provider_kubernetes_repo.go @@ -0,0 +1,101 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// InfraProviderKubernetesRepo is the implementation of the repo.InfraProviderRepository interface +type InfraProviderKubernetesRepo struct{} + +// ErrProviderExist means new provider cannot be created due to the existence of the same-name provider +var ErrProviderExist = errors.New("provider already exists") + +var _ repo.InfraProviderRepository = (*InfraProviderKubernetesRepo)(nil) + +// ProviderExists checks if a provider already exists in database according to provider's name and config_sha256 +func (r *InfraProviderKubernetesRepo) ProviderExists(instance interface{}) error { + var countName int64 + var countConfig int64 + provider := instance.(*entity.InfraProviderKubernetes) + db.Model(&entity.InfraProviderKubernetes{}).Where("name = ?", provider.Name).Count(&countName) + db.Model(&entity.InfraProviderKubernetes{}).Where("config_sha256 = ?", provider.Config.SHA2565()).Count(&countConfig) + if countName > 0 || countConfig > 0 { + return ErrProviderExist + } + return nil +} + +// Create creates a record in the DB +func (r *InfraProviderKubernetesRepo) Create(instance interface{}) error { + + if err := r.ProviderExists(instance); err != nil { + return err + } + + provider := instance.(*entity.InfraProviderKubernetes) + + if err := db.Create(provider).Error; err != nil { + return err + } + return nil +} + +// List returns provider list +func (r *InfraProviderKubernetesRepo) List() (interface{}, error) { + var providers []entity.InfraProviderKubernetes + if err := db.Find(&providers).Error; err != nil { + return nil, err + } + return providers, nil +} + +// DeleteByUUID deletes records using the specified uuid +func (r *InfraProviderKubernetesRepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.InfraProviderKubernetes{}).Error +} + +// GetByUUID returns the specified provider +func (r *InfraProviderKubernetesRepo) GetByUUID(uuid string) (interface{}, error) { + provider := &entity.InfraProviderKubernetes{} + if err := db.Where("uuid = ?", uuid).First(provider).Error; err != nil { + return nil, err + } + return provider, nil +} + +func (r *InfraProviderKubernetesRepo) UpdateByUUID(instance interface{}) error { + provider := instance.(*entity.InfraProviderKubernetes) + return db.Where("uuid = ?", provider.UUID). + Select("name", "description", "type", "config", "registry_config_fate", "api_host", "config_sha256").Updates(provider).Error +} + +func (r *InfraProviderKubernetesRepo) GetByAddress(address string) (interface{}, error) { + provider := &entity.InfraProviderKubernetes{} + if err := db.Where("api_host = ?", address).First(provider).Error; err != nil { + return nil, err + } + return provider, nil +} + +// InitTable makes sure the table is created in the db +func (r *InfraProviderKubernetesRepo) InitTable() { + if err := db.AutoMigrate(entity.InfraProviderKubernetes{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/mock/chart_fate_161.go b/server/infrastructure/gorm/mock/chart_fate_161.go new file mode 100644 index 00000000..4e2c4738 --- /dev/null +++ b/server/infrastructure/gorm/mock/chart_fate_161.go @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import "encoding/base64" + +var ( + FATE161WithPortalChartArchiveContent = getFATE161WithPortalChartArchiveContent() + FATEExchange161WithManagerChartArchiveContent = getFATEExchange161WithManagerChartArchiveContent() +) + +func getFATE161WithPortalChartArchiveContent() []byte { + base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOz9+1ccN7IAjudn/gp9h9wLzjIvwBiTZe8BjGNueC2D45vvfvZMNN2aGYWeVkdSAxPHn7/9c1R6tPo1Dwz2ZpfOOTHTLZVKpVKpVFUqDbEk7aMx5rI1xZPom6d4Op1OZ2d7G/7tdDrFfztbrzrfdLe3Ot3O1k5nc+ebTnez29n6BnWeBJvCkwqJ+Tedz26r2Lk/yYMT+hPhgrJ4D912V3CSuJ+KNW67rZ1W9y9DEkbB5LbT6rY6KyERAaeJhELvSDRBgeIfNGQcvT24PkY4DpGgkjQTxiWOEI3RWxKeHp2txHhCNOAVwVIeELG30kRjKROx126PqByng1bAJu23JCQcSxIenLR/TAdEwW2NqJxf2pW8zbql+tDUfWiaTnxtuv+rPDD/b3GUEvFkAmD2/O++7Gy+Ks7/re2d5/n/JZ4VOsEjsreCECcjKiSf7qEVhJI0ii5ZRIPpHjoZnjN5yYkgsVxBCCpcplHUIwEnUuyhlVWEmkjP7RWEVhLM5fQk3EOvX79+rX+du4nfhJcrVEjKVLMkxoOIhOpjJMjKSsLCHglSTuXUIFBRiMYjToRQnxTMAcM8hLYRWkU4jpnESjwJ827MhITCGZ6uWusmHRD1qxUTCUUQSrAc76E2/JKR2EP/+KeB3UQCeq37M5k2ZSSa+pWpu+q3ZivVthZElMRyOdRjpoCxmwUxV3BEgvnNcq1AlSWaSNJIYL5cG6bS4kMw5CyWJF5yrF21FrnHkyQiatGY0c5KopYOIUkckCrum7AwjQg0kkzlWK0xUF9PJTS0qxGmbf29CcTMylzj0R7SqxInEcGC6G9xEKUh2UOSp/qNnCZkDx1FqZCEn1zqDkqZnLOQXDIu99BWp/P6Fbwf8STIv9+E9xHD4SGOcBwoEAZVQfgtDchBELA0NtwMH2IWkh6JSCAZ11SULFLdARLDCzwc0pjKqf41wMENjAgZjTiLInhpKHZ+bmmmR0ukg0ugd6Ohi90rMRCPjiJMJ+5toH5plAz5QiyxRlsyjkfkKMJC7KGG/qxr4SAgQpwxRb8rgsMPnEpyEQe6bUF/J3uo+wM1LQBBJzjGI8LtVA0YJ6KfEN6PAUp3x3xQP8Ue0uT0p1K5zmanqg5CE6waNLNqr92Gf5vm7avOq1emXMjpLeHvmJBaRA0jdlf6dA1cYRmYcXmG76+I5JQI91qX7knMpWYI834QseDmTHe8/DWZAmKXPlePw6GTZmo+mZ7C63ZbvYEXrzudjjen+gknQ3qvK3I8GFA5+c2CGUP/7FtLonjUTwCZ7sudV5te9/aQ9yIVio6gQtrWhLhjPPReGlmUa02/K7W129nt5Jra2XmpX8QjGt/nYcAr+0bKxMB4veW6riahe9vtrOQWqJpJXiE2XJ25MqNKQigSacmHwwmNV/JU0u+Ka89CaOka8+VYsoe8olUoLi1mMtGhIc8SIAUp4VdYVEqsIDSZit+iedSBQgWC7D5erxUlsyY0X21tdbRgUlJxgAVxorc/IUZOFiZJfor0Q3JboCm0saBMhrK1ItmDtAStYTmKR1mnu683W92d3dbrVtfveqfz+qXt4JXTV7Ml5nfGbghJSG7ue7pY07bUdCVboJTa10o53dvs7nYzadPHQeSWfiv/a7jCCHRT2fBIw59CvuBv+OWAdRq5yWS/c5JENMBiD3W91SXPSBWsVGSmap7UwKzS8NqsRHeM3yzSEV1u+Y5sujfODmDHiJPfUpIbNYSCJFUwG96rCZkwNfiNzR9o9j6iE1pVdbOy6nZW9dEomi2XZfZwi2Werj5ZxzhkLGnakiXCbrY6rU5Tl9psvWptN3/Ft3j3yfnCrXDV8mHebC9qYEW5oajmhNpiJLIlvyaJnpQYngKy0OqcaSezFuel16DF9iBb8L68B9Fkr9yDkPtgjONRNtSe3O92Wl1P0XLwOlueopW91a1wlkrSl2rroY0S6ul2Op1O9hOhhLP7qf+iabQ7v23vc07VAyOV/81T+NS33eybVd/35rQ0q6lXM5raXFnJa7oLMYmnBvsz5lWr85Tcse3ei/yH1/XskaSDiAanh5Y9Crvw+ulX3CRCj5ffJK6WGHS1jkVXM/3EyclVFLIJpsaO3mJ8NI9Hs7Euc4nHvo4BtrOXQkSX7v3rEq83GvAKGIYJOeKkdokyLGOLFcXJVmvLEKfMKatlXlldgFtWvYXGvCjwg6fSCipJX7sVCppt8Us4KL9b1AJR4KBlecc3VC0+Mz2PSdNWL5Df+C4eeQCWGQWlAFMJ5XDUI/yWLCN8/C4KqPxFOrhg3xbihWLvl5crdmppI09uotkfGtWX21ubuddvKljafntfPT0swNppAtaAy4JxwM637L36pfsCvT4iCsH2LebtiA78cW0HhMu2LtUKuPQq/UimC9S5IVOoo3ft8xrSpVxD+uechkwd1xCe2wh2DchIHNt1aE2x+pp9rcdsd3t7y745YpMJi/WmGWB2W6aPvhl6ZUUtNJLwGEdnavN84vb8hQ+X2fa/8OVNtSWgUOp9ZhQoQi7ZB762W+r5+UIP+H8lmSQRlkS0zy+uj3steS8fs4058R/d7e3Ngv93a6fz7P/9Is8qOmLJlNPRWKLNzuYm+unsDnOygU7ioLWyurKKTmlAYkFClMYh4UiOCTpIcDAm9ssGMiEjaueN1lWBhvnUePH9yiqashRN8BTFTKqFBMkxFWhII6I0bJJIRGMUsEkSUbUeozsqx9CMAdJaWUU/GxBsIDGNEUYBS6aIDf1yCEtAWG0z9trtu7u7FgZElfLdjnQh0T49OTo+7x03N1sdKP4+jogQYHainIRoMEU4ATvVICIowneIcYRHnJAQSaZwveNU6QkbSLChVNRaWUUhFZLTQSpzhLKYUZErwGKEY9Q46KGTXgMdHvROehsrq+jDyfW7i/fX6MPB1dXB+fXJcQ9dXKGji/M3J9cnF+c9dPEWHZz/jH48OX+zgQiVY8IRuU+4wp9xRBUJSajo1SMkh8CQaYREQgI6pAGKcDxK8YigEbslPKbxCCWET6hQAykQjsOVVW1N09pWuVMttXIxCAPC0crJEIZ5jG8JClg8pKNUEdM46gE7IdlGxguGnDgMUeOvasPTp0kbogL6NPkb+gf661ssySHDPPzbBvrrOVM/2M3fWq3WPxuSASZg3W2tfPzY1EzT+kkHsphWW87vCwXRp08rPSoJ0voben+y8vEjkuxnPIlQC/2BaBySWKJNVVDBJHGIPn362jP0aZ+C/O+PSZQQLloyebxQoDnyf3Nrc6cg/7dfdrrP8v9LPM/y/1n+P1j+f/zY/k5HgDrqRXhAIoG+axsZGpIhjQlqgBFMf2yg5qdPJhT040cntF2cGPoD/ZYySZCSwy6WrFjyJMyVY3ex2t3YSKIVE+Jh9jtjwqkEc4QCc6XN4a2eDoJR1Y2FPP8dkPn0aQXCW/UnHSltP9hFQvVHE2OCZTA+nUUDr8STEKKE08oRJ1hqdlBt2VkTsFiqvSLhNhoIYR0OpNg8FWSl1IFy1JDugipDhxlaxRC+lrHdQmG1O/74UcHEaSQrem07EQlSVaFh/mgUe+v9/bWl6p/nKaz/1mmRC1z73MDg2ev/9qvtVy8L6/+rrc1Xz+v/l3ie1//n9f8z1n9f7Juw2JYWHS3jEVBS+obG4R46gm3hGU5WCodOJkTiEEu8t4JMxK6RPnojuYKMVqHN9EpEnbE0jFw5wMO0llc0vF3dtkLEtqKDV8xCIlqqnT30h7ZVg8TTATqo8vn4EdX1OVf5j2zJqg37bJj1TVfUQZuwW12+1Sw+1G/ZCnRYLTWc5oxB80G58FBUhWSiPjwQyRLkbLedeaQWQTQf/JrHU31rTbKPS+FZDfihaFYG3eax9YsUaTsX25nwnxBpwwoDGocHYQhiyD2dFvz3ecPrhyMXRndq/4VvS47ufLiWy33wRtqUemTU4r4SI6ArWVlyx/hNf6KDya1TrZ+ds/E0eYumiUNsefGN3myGyAeLr4IVEkE4xRH9nfQFHpK+huPHSGRhKLoTZjlOhUSCSMQJjhBNENbjt4G6m69g5LpwjM+MolrA1Oot0kTxJgltbA74MPPLANjwnMb/6ZMD+PGj1uk/fbI4qTcZWxRiqncrY6p3svgKgwxSmxhBJdEBUE6FRViAnsB4SGNYxHQ4hKtv6zlAqqZam40TbQOUFdW6qihZwCJXF5qqqwiUs++AnTQo1T21TudAbrjRpQJKuDZcRx7STAZ+kEp/7FxUC4qJVAyKJjaILmuZ8QmW6yZ6xNLpBZoQHIuMblqXI9kLa/jV1GZD7U5kMen7KGva2cr6V33N7OiM5V0cCYYCHKMgFZJN6O/E7meHdIQieqM1zHXF3o6GRlVkScIAVdhuKhaBTr7IZofutPuJvPNi3mGMVcf89/f+q4yBc+89Fr6/t3Q2EpD8VieszNEaZPSHRjZRzMj45xHclrlYyI6OX86fdNlEMozYVr1oK5QzQMBLe7aEq3kiYSBgRAZOuwS7P5D7VzYoDG0qlKqpvrnAkL6QWKaiH7AoIoHsy2kC44MnRBJ7YsO03J9Zaw9dvj89dWLPO3VhRyt/sMJGU7lDIC6k3nzWY7/mCepKv7snootDCZ78Vlgu2PBd9Wpo10ybOtCpts33IqdaVreX5guBFui3AWFT4YxWbETA3JaScsGGDSLwW9Tkr23vJJnbEvWLmHMGfpeYtdFV9kfx99y+5As1trY6O96km+B7tcTHJDDnDbsu2E9IHJG+pBPCUojCczzYZ7EVfu6cWeXps/JZsgr1wAiGPIRWvqLXg+5Ohr07krYwWCjvQdvMgFmZkiFvjnTlZY0dlddbr/IU8Q5R5E5UaKliG1RLRO/y4OrH/ruLs2NE4lvKWTxRu7lbzKlSMLz2J9mZuGVoqdW9AgmXoZoGoImVaTLeobncsbl6OKpKy5X0+bDqqF3DxzEjWTujvHcIb16rXllvjAuH9jxtrwaWraE3sB5+2XG7BSrbwj6AhSsXK2phukBFkJh+iy4kapFWrRCsHBO1ALZt2b4XAAx7BVchFxlcW2cuoPzZx/mDZo5gF4dsHsVNtSK9FxlpU7VqnCVLaNCXMtpDW3WU1NWXoWO5xhwguaOf80kIxUsU9HTBuVVtUb++pzPOq++K2vpKpY/6QjJO+mZn56Stjmf1zxVaPeeLqDn/PvrAv6eiVqHlVCs53cy2kwu+KZgynHlF/xSVJzPNVkjtDtW4AuUdnZu69zRBnz7tqb8Mp6/V7nyaaG2tbrvjN+VbWVruRKgt6x0m9eKEsmLOtLyTh+41l/spOY7FkPCynUitv2JPCTrKEePw23XFZDlou53PJGo7UFYPauNUjgFwY6XZbC5vcXcbo/lG92wPtbTd3ThWFGe1Es4SwiUkLfjDsYixfu67LZndeLdSHu0b187nG5z2nH1JJJzGIxBd+iiqMf01IeK/eY4nZD9gEzOLgl9bv4aDoPUGyvgAFGotEgcsVL+CMeaCyP3312+bu/XFDNL77tCEIYFkkwDLVspp05b1QGXZZDysFVpNRSP1x54+vA6UemT5uvckIrUW0Udde/5HDQsOJOHHlqqpHO7+t3vdI/KKiDSSQn/AqWRXxIhDGKX/HmIaXdwSfkVweBFH030wtf63HrhrOiG/s5js/3B2/V+bhzOGy+Zo2H+S5aO+XbsM1Lb7aAtKBT9P8H1TjjnBodjv2qNy5SLe8rO/6Y7UmQmkikdEtiZpJGmCtY+nOaQRaQr6O9nvds4O55c3h8xtlUKdnDBIadga0kjCRluJRjdrMyN7Xb07MmgKiWXTAFi8JtS6peSuaTtQUdecsmETcAEroVonUPwyEzohTTlNiNj35HH7V8HiDf/F/STakORetsfS/uVeJRE254RMA5rRIjaicSVvW97JmNKWKrCKD6iKWcuAiruvVQNJiKh1Q6ZNUIP3AyXOIdtTfaGma7CyzOyvOKJY7D9s9dXbkSZsR5rSGDYe2+lds+exq2+tImf2a85vYMhsJv1e3trcqFTV7Ll4eyQWSukf7qunK9X5BjUiXgdsYQ6IfXtDphvo21scob39BWvrlqGqwrk6InwnHxG+UtQ4K0LyFCx/p5NLP1TINOSd393ZedktELRhW/706WHMZS0GT8xetYYJxWAKcR9RnCSifds1nXlDkohNJySWtbEgj4iwSEigEx76aVWEVMr1KHeM9IoEEDsJPl7/IKoXxGmHeRGMcrGfRf5CyIbhmTY8SqgnyjX3YBLsZv5uTQZUdsYXNerajRaNqTxiscQ0Jtzh1rRnfz9+LJzLUK9b1gGOPn36+BGp9U8OUeO/RFvhCe/0NKsQ+kYzVWDmKqcVda7xyK+2mzOiuryWNu+lB0sjniXFzBmINZfSeNT0TSoIkfjWzzyky5393Pv7af/NwfXB4UHv2H1GCHKwGsn45Ipwowav973jqyVw+iwdtQ6Hy4Ne78PF1Zsl8Hg0nbUOp3cXvesl8Hn47qqWKBdXyyDwGXuxP3JYBGwywXHo83F7QOP2AIux964ZeD/+8BAVRKLmvfdimMag2aNgTIKbs2nv76frLz56BZD35Yilsdzv5L7ejWlEIMsA+h6FLPetXPfbfxTe/KX7z0IVEowZanzAENMJgZJAHiQk5pKELQ0TQcj7Hvq2AK9RgCYiQhIEuVlyryWWZP8XgAxn7EFgoOYYffsxY7FPqAkHz/ftSzXsn1Aztb/V/PyEmon7bubKpz8QvrtBax9BmqJvtz6t/VLAgA7RP1DjW0Ckgfb3UQNH9JY00D+/R3JM4kJxSxdDC1B10RgLNCAkRjhSm7ipo1GRCgNO8E3h3ZD6L0IWE+/3p5XFB6SVay4bjzqLX1BaoLLZ5YV0GTI90cplg2KLS5cfZ1+3gvl1C0sYRO3LxqdPxS48YCGbF5PncrXl6lRkcNOAFtLJC5Dz6NRp5gDNKTTd7XIncuNf+zK3QGc8cXnxpu8yfOkH5O1bziZ7JaYmUXhFhsX35ovONaMjVVoJC3NgPyP+x0f37cH1cf/t6cWH/vvL04uDN/2zg//rn78/K3dgDzW6+nRCY1E4hz9fH/dqIG3v7ux2Xm+9fplLrldJaSXURJ5GzWxeXhZj+2pL7PolSmsTqlmfUHGNQqV1Sj+l1Uo/k5uQctRMUFvpVu2Es19JIPVhmTZ4GEtVgvrC3XbZEVAPuVx2ubZKsan1TZWKllpand1WQhMS0XihxsplK1ob0vssgk8V1HGrqSCoMEH16IWoSVFDtMe5/Mnm57cf9cT+1B41Po8ICi2L/3y0Vj3EaOKhpX4shtQCxFqAK/Thg83WdqvbHNA4y5Boep47E9G0m1Ewen4WKIAwY7QcQWYR4xFaLKMAutrL70sfTNR3C1DrA27ur771Uk19fZxF6UTtxuOymIsrM5lnz0TV0gtFFQNEbCSKyqRNYlb61pwHTU/UArzSmZvFgX4GTy2HhNfreWO9EBEq42gqUaoxny3bXtn6Wk2AKktw9qyi64s3F5A1wzWK2BACbXEc6sCPZgDH85ZD8lc2KFJ8Hue6ESnVnduaDlCJWICjPmD70KZ1hwFQMw+oRpnN/AbeAbUM76fS/71mi1sA96lO/y/UfdItQCEaYEkdzsumj+aIxTkM4jDQs6c6cKCSafKRDLVSZAGIX052V0apVDtjTNyXlxsxq5XPmFjpytitjVWZ36iXfDGrlMvI+OhN2uSOWY0s3ePnN6anQvFCI8+8XbzqaIZ7qNTmnANupVQKGZCqy1lmxB9XQirb6mug2jW5OrtDq+D50qA4EfpAoBYuB9Ednlpm1nO+wtRibNClWRlYT1ZeQpQr0VGN+WY5kMWzxXmIc1b7mZBn1m0ussDPRry+ZulWq+VIUhH8lbEzHcLxMefgzO5GKnuI5slMMknk9A3le+jjp7zwy7HqPDAOB/kTcJvO2JrrmHdhRf3UyWV89dZUZ5ZT7ebMLt5UKLo0nTfTpJWpCbIzh8hmBddBkaXdmd76bKnXkEHSHPEksAaa0ilKifmIyJIBJjvxdX10WQY5ljIpgtytBrlbB7KcqDfvYv0cf+r2Z45P091e8689THMtiHCUrmFTrjdQ49TLe9zIKw6Xc0LN/dz+MxbZJ2CdL9pNP3n9Et3U7FwPFlCE2ivz7Oz53NS6UjFDfqO+pVL1xkoB/6eZa35g9RKpFdZjgta122rB9BYvvKwWL1TrD53lADi0kbyPHV7iTfSPH9G30NgMH4j+7sWNa//JMqRUeEj0B0pjSSMLys0+YzbRwc2qZlPxkOftdScmvq3O1WHB4zDUemF50i5auRSOT37LV7TX3C09qx/Q/KLiSxMQ0mI0zRnJhehYnUhjOXIuB+OJiLo8EnNImz8eMUeCVvbE5N94EmFmMKvbyFUakh4giqyd5QnlT24FzsxNOX71TDc+C/r3Iy665GWUkV9ojCrWnqp9yrq/kZm1F4BlRQ/eZdVeY+EMVmbj8thjm903YW/8bM6YOVlpPRYFl33xwjV3QnDGXKS/G1hzrBze5Rkon4CK/IYazcbiFf03el/X8OKG3faxXGxGL2qwcyKpGPJr/7R/fe3kfY/wFPI/jsOh/l8zjB/tPvg5979v77zaKuR/fLm1ufmc//FLPM/5H5/zPz5y/kdIKfE52R/tzY5NEt/OWDzdBZBLH1U4urg67h9dnL/tD0XfGP3e9vZqMm3kKmgHdx9O7AtFmyAVhPf1WfAGZ0xWlofDHFBynJK+PvuMGt8tUHbEWZpUFKas7x3q6gcsJIHYQ4yPLNdrcC3K3OmvVi/GSTI9UmVXEHr35m1PAwuHon9HBqr7fVK63yhfzmMTryycSisXtpTs28Gy6e+Aq/o06feBGpD3pN+HYE4f2s8HV+ca2hTzuB+xUb/fx6MRJyMNoW9w8NCtqKNjNvopj9Qga/kwpmr9n+pPe7vd3V3fy2i+gguv3ShDtTqU2Qj2OQnURJpWkG9OTZ2lIdD3i5WHT9Wx90YVqrZso623NCK9qZBkcnXWk1iSngK6QNvBmKhJyx/WvqveClQlKqetI/NHz34qI+E1agr31byxE7E/wfd0kk7UQEcRC+w4TwZ7aG23+3qzgqYPBHkLaX/20Nr2AuM0FDDhFZvqIUs53UNtPoG3C9Aaxkd1hEhOA9Hv9+GKSzEmfHmusZNmDxW+zK9qE4IUa+7tduDCuYV5ZhagziJTRv/u9yXHwc0cgN0yQEknOiyu37dhe3PpWFFnRGLCaaCYI5v/Si5pEfAgmNnw5MRMru4EJ5yEaUDUX32WyiSVTqZXtqZrVBXX8r9i/uJgxgJQ6IeSz6XB0Zdl2/nX3dnarZoslVWDJPUn2e6ceiEVN4rsBEdybBcDwtXk7fft11TSiP5uhyghPPuUEB6QWF/zsPZ6t/VyLp4TBtMZJ2oZ0mtLSPkeUgtB0wRozACA0/tsyMUeygZUjNPhEPyzZweXV8dvigM+5HhCIDusZhIFurJwPxjTKOz/im9xnyVKaVhr/t/kfrvzemeyVgteMYgZNxg1Vby+tK3kVbBith58EaOtzqvNGRiZf4q1drrb26VaQGc1Jll1POmT+HYPvTt4c3Fx2Tel312cHe+3WSLtleBbrc0WRDfW4/0IYMw/S0D6jNOv3i3nj6cHP/0J2HqcnuwM7FJkqDwFO+ugUOG2efSER4Vg/2TbK4cLFq6/rwsarIBSCBysuSf/sSIJa07X9I6vfjo5Ou5fXh27nW0ucsMeMCltwjzAxaM4zSy+peIwTuWu0j4LHU3JrvWuK7H7ak6Jl53Oq0ViIx3DFUIX/VBDPWbtcCjaXmRMbSxdnhUWCB1EjxjIl2/8S4YQ5lv+E0cS6hcPj9/T9ZcI2ivzYN7SMycGbG541uzgrPy41cdouQldiNLK0n65JivizjIr/gMChZ50Va5z3PlyMCnKpVzoTPZ6TjSOElsFkJkgy4HMXs8BCXIuD9MXfT5Q//3CzsWCPFnAwbi8LuK5GB/sVpzBxZ/vXcyx/pMwoe9hrHcwFkTs4/kZ84B9d2OtxTkrXenWm+FznFd9hutxWc/jQph6/seyz/GRvY+1/r/4i/n/dna2OmX/3/P9b1/kefb/Pfv//tX8f3bnNz8VrS25vAswYJw0BZWkdZ+lwPtr7q6Sv5l16q/meNj0b06f/Ktq+G9KlGttFPL2/xX8hl4h2Mn+zfgUzU1KsJv9a1t/sg208y38tV3A40Fp3xwVZ3tRH07CZy/qsxf12Yv67EV99qI+e1GfvajPXtRnL+qzF/Vf1IvqlNzH1IOf3otaj9OTeVGXIkOFF3UJR0F18mDP+0AiIkkzYkI2hyyNw7LvdZCK6YD5GdtsUjj0j4YYNzZQoxmo//MJavJhznOmGmkr4H8B4I1/LuCcq3fP1TroVPGSYQ/N8TZ7/Fro8dN4m217td5mW2CmtzkP5V/C23x0+r53fXzVPz84O67yMg+xJJ/vWva32uhf2LdczLtDZGBZNWcPKXTS5ayZVajCdFOdTWf5mTPPx+v47mu4tl3jX9y17Vp+dm0/JB9NLavOTJVStk3Wc/dSTvNHyGqS54p6x7nrQ0V6k5LrvDZpy2e4z59UHXta9/msbBQFefAYSSnK8m1mXop/C+d+noJznPvLq8iP5tyvnmmf79zPTc8nmSJLOPezReaRnfsO8ELO/az0g5z7M6o/tnN/HqZf0rm/wFPw/9N4BBaux3L9wzPH/9/tdDYL/v/tl53tZ///l3ie/f/P/v9H8v+Xbo3yJGi1aJ+bQiUm8o7xGxqPWje7okVZZu870ZLqgUlVHnCHbfGOLCMpsz7gOGbS3wB6L3KbooUglBs3K4NdxE316iWp+NGs2YrqeytV+aXKyIB7Xddrelegx16qUphpVseDO4xdADHctKgqOANQu4Fa6m0uUhjL8TUog5ecDGlmyDNXX/i7H7MHnJm/MvcN9NOilSKdDAh3aW3q07WW6SEjQw0ZLTicqkbtMBoNNEsyVDVBdB7CR50dc1MbmgKfOy8M6g+dFBXVv9KMMJj8iaZDzBQHspuFZ4O9ZHf+dDDEWHQueMUXnQg1M0Fn/3rMiQAQZ8wD/f1zp4HG+6GzoFz7K00CjcifaA7oZH0TLCThj78qaHIsOguy0p+3GpgbbB9zErhraWszlOkCnzsNDOoPnQcV1b/SRDCY/IlmQu7m4UecA4YSi04Cr/hys6DK/iaoBEMojnrm9vhIHOc2HlW7Dc5iSeJH3mwYoLP2GrbIZ281bAcevNOoAvC1NhoWlz/RVPIG+5Enk6PGwrsMv8KCE8rXsrIfX9sC9fx8zado/wUbzqNaf+fZfzvbm53tov23u/N8/uuLPM/232f771PZf2uUK10O1CscJWO8ZZSsH7Akd3haoWR5+kaCuZwaRaM5MjUy3WtpV2jeswyo7VmNx4BHq8C18M2pGQGLJWeRPpqhw9JBK2nm9IFMD7BhYSacXcqk5Bt/d32tXe76YJNxzDa+ayxjPS9GYyxA958ol6nWpBfPUr40oV2ncr5m9TbriNZ8DNkdBdTn5uvXr197451phE0EFyL58T0hEZLGwLd5xSy7F/YB1uKcHrWQrfYpxsKz7T3FUOhOlMehucActGOSG5Ga0dAjUTJUFgchb6CcNwJVG7wnmQ/lPd/Dx2DZHVJ+b5SL/ylOnMWH7EHT6EGboao86l9bB/pPfgr6/2Qqfov0/5t6Wj3CZmC2/r+5tfPqVUH/39l5+Rz/8UWeZ/3/Wf9/5PwPID3mXoqTZTIoL7Ba/szN/gDFls9bMPc6OWMURObKp4a31LqAQH2gq2nvJ1V90Hdyipb4zSWUaDZNQcjENMCKFbRFG2JGxYbbT2Tfhb30tK+AApijq+OD62P05uD64PCgd4xO3qLzi2t0/H8nvese+qUiFFGPgQP66dMv+pZ7ixWOIsQSczTBja1iU1MFCr7vHT8Iuj7ZDUfEGfd7cH1weFpCP1f6F7Ru1If8+z4Nf0G946uTg1N0eXVydnD1M/rx+OeNfGE5Tcgv6KeDq6N3B1frmy9fvoCWzt+fnrqSisVEggO/YKfTyUqiN8dvD96fXqM188darm5dNVcowXI8H3YGVDKJo77S0ChMtV/Qyfk1en/eO/nh/PhNFXxTkvD5zRz+fH3cu746Of+h/+6g9y5rVRAeErEMnrd6Dteg52p0vDHBMhXzxkNPkLCP5S+Kw4+vT86OHbCj91dXx+fXffWyd31wdumqpUm4RDV0cY7eX6py5W8A8UVW993B1cHR9fEV6h1fowhLGnfR0cXpqaqsf/bFHQmpGPcD+v2Kz9/vz0/+/v4YnZy/Of4/9AsN7/tpP8/GsejHvyhsSmzvMeZ6d7PzYsOw2/rOdufFi+/9Zjz4eehC1sD2pgcMw4LwFDPOw/XVzquFwc2AthSgOrQMxy3TxdsaUJbbX3y/spKTa2idJWr2vVhQsOnSJbmmXy8m1nwZeHjyw2zxUJBRlXNOie5lpn7lHC1VWHKyzq//hWZtiTfM2Ajq84Ybx/KglHgkk+YLMokrX+IT96WOVbKNtmqdRMMmjQNOJkrvEeS3lMQB+Rx2YiFZrGQe0+qyOcNAs4lcHURDpc5jTb/nFaS8gmTkFbQvEtqPc+zpc1B5nDcq2WkjG9450tJrPFqi2YWhzujL8hjOwC+3PLg5647az5yoOvuVKutP0uztPFk+S3msEroGdKCvSJ85ByuUrzFTyoBtrVujqjIu9VStnveL6NOLzdEIC9kfE8zlgGBZPeegJzOm2X/gcuRx17if9GOr3+V4UY/1+k4HlEYY0w1/ANXoz1CH/DZmgJ+tnnlAREArwZT5eSFwNX32ujdb2/Mxq8arRioInRQQrU8wXUTbMzkEVemcgNCv1fS1U0SR8nFExEJzT+JRvkhx68lZ0IeT9SAL3FccSHpLaj//B83CbGQzHsqNth2JSv5ZeL9gYJZ3DLkPwEozNw0VLAe89LxHeEx2KO4SiqPnj4OVnmXmULOLCMH44vzhqngs4t7N3VHWyKOi2uE0mkrtIGvuoSrCAtLJ5HLqkzhMGI1lHQCOYzGE9I8zyyXF3jjGaHb/c7k4G8kiI/tsVsnLCvDJee/46lqR9QJ5nIPWldKwAf7gDeTW6g2kWeGFzr8k0PqaUQdMNsC1DbT2enuno/61uZvODs4Pfji+Uq/eHR+cXr/7ec22LsbsDmmzu17/dTgP+g4NOZv4COnPhdDYB2aT036HR/NLPH0euRqEniyJ3OIEWOIerqdKgGbcVcXEZ/C6Lt2ZV6eQ5my3gR4po1me1+xTyHOW5Wc5+7n399P+wenpxYf+8dnl9c/9y4Ne78PF1RtUiMewF2x1GzMAWV9Tdd35PqE/0G8pk4VMQsVG3veOr5ZsIBWELwbcdn/JBhIsxB3j4bxGFCLVoDlj0vuySBK4ra3OjldifvrBzEM6K5layIIbwpsklnwKK2OTxlSGg1bYroTqXSZWBe4W83ZEB+0iTyI/R1wtYU0JND+7mS7/hZO56Ua/ZBI33eJ/fPK22gRt1Tw+MztbIXAgw7hwjmxOLraa+VCTCK1ZyplWU/+zE7xpjqlP7Ka7X5HVrVbrWSBZ2xNpO3Vp2vS6a3BPitLRzzDmvV44wZiZ5QskFltSZ/KyijUfnFasYnw/P51YxhSPP4h+IjET/VFP9UfMIGZWFC9zWLM+Eqk6EVdzRs6wunozcoU1l00WNhs3L0tY7ujgo4fOFuI/4xGN7/X/H+8U2Jz8X9vdbrd0/9dz/q8v8zzHfz7Hfz5y/KeWHp91AZiCsMDtX6rY8vGfcNChD1YjkHE2WNPoUXuZTnE/9bXS7Gx6ZYfJfTDGsdK2k/yeEW65SuoyvBYqq8L5FK/qGfEkWBiEKuyDKB4DOVELx5xeAkhU3YnXW7mM5R5ur7e6Hc/8NIzYXRm0/VILfbcW+o4+P6KPy3x7Q6Yb6NtbHKG9/RqaeEPtUUPVVCSo2T7uFBZbpKnRUvxoeQX+twrGCBSzAQun2sB4x/gN4dZ0SgRCm15o7CrhnPF+xEYIwZVY8LsVsdH3c76rViQNjBnTK9YOyW1byJClEiEaD1ltCcK5ee9jlNDQkhpa1F1NaOgV0hBvSSwF+ujGxnQ1YHFMAi0jULezua0rfipUVyPsVbYzVj8TOtHZfo0V15uNYDZGyLtTqM0CSWRTSE7wxMPSdKE/ZHyCJUITtVCgtW/NBTU4DDlqIvsThu4f30o60QEr0T9R41ujgjZQ41vgSMWwDbRWMHX4z9q32qCNvlU80B9MJRF9objIgOBkSDjhc6CYwnBdHR6ROEPhXvXnDvOQhOqvedikiaZL36LlXkD/Z1c23YebmLyanIiExYLA+1kQssFzf2id3+No/UKztBqhwgAKEoegGZiHxRnQVRkk/ZglqRjnvrkCN4QkOKK3GlOYETsvs/qwggm4R6lP4/6YYPW7DGZ19DtN8k1HKe4nOLjBI9KH3CKNbxNIJNKOUtz+n1aU4u+/b3zvsTeVfTNBBtO+qg69WlPF1UeKI/o7UfXWCq1bsmuZaLwj3sxByJ43zmSskpnfK8zRr6mQSreg8S2OaIjMXWAIC4RREuGAoDGLwoLpcmCykpdxtV88TFF+dudwBmm9KM47XxVnK4Fu1SRvfwfSvSS6TLc+VgkYxY2wehZkTF6ozJovMOms6QJlkiQTIuZvTgJCb0k4H5p108EUbuRn/2zZocWQq5Ah0UCl1xaf3CezEEDTjSJXe3IAxIAMkqYnChwtS+IAmCbPSREYQdDr7ubu97kPACGHh5IBXVFVyn7dqvyaqO14d/MV3Nna3dvKNfXJcMlnXUgFquujqbZf4CqqaoSe7h6qhQmwhAsxT3v7PJVj0eyDio5FeF3nWPTqlByLj+RXtDdk5di+idoDGrcHWIwL75tB4cUfBSEiSIiaFDXEqhKh7eLuSr+dsJAOqXpZLjBq6CGBtdT/qlbVQmMkGDPU0BsdyOqF2LC0oUMiBdHy/2sUarOExJwIOUXNEVoLMZmwGLHh8HtfMC50adPW3Eubsq0QWuJGJjU92glnv5JAijYY50AiGcMc0DLbjRS6V9w8F12DzkdXA2F5VLxhXRCXWm+YxukLO/10o1/S6adb/I93+j3sxqZKzp59XZNvSXrITUVPs05XOb+Uvl+4rmer+n6i7HX9UWbD2Y9xB5EGpdA7n3f/0MwLhNTmoNjBbnUHu1+hgwq9B3Sw1tXoIQiV691UumTkYX5yqevk3+2hRm07pdqNYnaY2dcoLafebZeTS35tR8bz86Cn6P8zGZnaJpvVo/gA5/j/Ot3OTjH/S3fr2f/3RZ5n/9+z/++R/X+zLy1Zwkjy6FeVPL2ZpA6jJ7OTLEGCf4FYa8saRZuIfl9nFPFrFawisGrJxzKNFNgu22E8rsUEgftTbfdQs0kT9O3HtwfXx/23pxcf+u8uetefULOpFOTch8uLq+tPRXMISmhCIhqTx4H2a5pMJeEuLyMA3O+AFbRjwOxDLkb0/xTrNps4ithdkzOmEAnJIB2hZvPcgDpIkpYF2w8p319rH9/jSRIRsVYFLGbNAWd3gvACEMluSLy/tlZ4bcO299d8W05ttLxxhBfaLceGW4e5DQaviwbPk7wabMP6Pqrj7vODUwPi9dZuZ0b13vHVT5Dqpx6Jjx9Llymo3W88atEEpl/dZzX4elthn0UsZfZqIYfGgoYwyxxtL3iy2sbkFaizd9XLlMWD0k2FBQxUj2gtsvcnfUEDlb1469lCtYSF6uEh5tXcu3SkeTWYzw44N8xQH3GuCzx2yHlFkmP0SKpfXdi5bTJvoPKll2+h8t8vbBCyk3mB4PNl1chHiT6vGuzPDz/3OOQJhnPBAHQr1B4vAt2uH/NC0G25pWPQays+ZhD6HOwWjUJ/Nv79aZ+C/S8RyWPf/jL3/pfuq1L8/9bO9uaz/e9LPM/2v2f732fY/3JrXsLCHglSTuVUq811l8Ek8LV92x0Qia06eFmsnVMmau+nFMniWsOc+2QyJZHTWxqREQn30BBHQpsBT2mc3htNIVVtX6XxgTiIp+prmiQR5FzE0Q+cpYmoKcjVn+8F4TXfhwKq13z1Nh5NtPbdmrcaP3D+F+U/3NJo/nmslWCm/O9udba3t4rnv3Z2us/y/0s8z/L/Wf4/sv+nfE3x0gfANIj5J8CWvZ3YtiIkjkMcsZjkTvWs6v9nLC+Zz+89M9zoLUvjUKfOXz/ovX1hiMNiYgAwjiaME31LmBp0xlFkuVSxklopRAu5cTq/uD45OoZJYUD4/GJmBBVw6AYGE4chpDXFEZz64RPssg2vIk5GmIdqTAM3udldTLgY06SF0LXqU++tRUlkM9IAkAwm7eIz3tTz5v2Ss94AKM19tPDU9wcQLSME/IoPFwYGgBUJDxAIloY5sbCoUDCV54kGVCcZLPqLywdfOpjaFTIiI67+c3UVNZtN9AOJCccRGnB2QzgSRCoiCvi2uupVqDGrGBljzzxao4UGdwRWjWt3JfS+5CmpMJGUrMamG2dECNX1Hv2dGKfvvXmnXu13t7Y3u69ebe76lf7/jN0QkhCOfksZTycoOxiH1EDHOiHJ77aYvrha7PswtIRMza0cPWlESAWYwC8JBcvwHkI6Y5O9ZFzRb39n52W3knCl0qpoZwZNIapOMi0JmD3Y8e76+tKa3R6A8x0ZFBDe7exWI5wvum8uZq7D9h0TEi5ZYxydXLoTSbCCaihoQONQIBZnV7hQgYyrVLO9KnGga1of6pJt4PCWcElBQuuViKVS0JCodSAKW+hE+xwEkRvwGZx8SjaexESatlsjIk9ZgCPV4PoL9VP9pfY86y8U0qlQOoNCyrXn8PYRPod73LTk5QSHwo6nEiHnRMopOrlooTcZOQSBMd9E36lNjKQTolo3f66/aOFbTCM1RS9tqkGxrtNgxunk5OJaN7M4EoyHREltck8Ctea2YKUrvrV9VjX1HThG53Rz0wnzNBgjLLRz3X01xuoNNCISuYsw9KaWEqFTD2aliQxa6EQiHAnm2hVJRCUapHEYkRzJdm33LzTWxwZpS4vdhYnhzy2BzIlkGo8eb4TeSZlosbPISKGEsQiBwdzDUs/sbB0AxjfLBoxeNmpxDtAdjSIUMjTAgga2fnalUURvCITPttNY/WNovYF0gk6k802GpyQcEa7lrh11U0KyhAZtkQ5EwCnkdG2buCGFzcSsESEVCZbBGCk22lAIRXCuFIFvR2Gixt9A9gi/PLXznHDJWASL0YNJ7vEzjqIBDm4s+T3qB6D2uTHwwdpZpEAaZ6nSEkYRG+DIg26OJFbQwER5x+nkSDVT08FuTmqe4Xu1+AUp50qrvyMDx+N2qT5ynxWDXpmv+93O5naOWCB7tRppj6xKhu7GNDDKtmGqAYmY0k0k00uvLquq72f7CB+yVjtswTWBhphGKSfNkMGhdN1EgGNPBzSNKVkjGYrYiAY4Uoq81eoNjDcAwmk21jpVoYbYDMP2pCON0YRGERUkYHEorDbyo9FGoPC1LnsG5fa3nJczA66LexPNA18J+cKWNLB7utD+Vg1ky3EJ5VMAXgvZMI0qmAHNQVVNwphiKoFPzYiOOA7IMI2QGKcyZHdxCx0Mpb0CDRolEU4EEXpZNcJTy5wBQTc0ikjoK0IGjqWf2N8p0u5thEcg+W9o4sSdqYbuxiS2L8dYCSqBLlKp2HNCJoxPdeIGAKUAHOZavYgvLs7KrGDYUM3siI0ggAmjYEyCmxY6VnvGgAiEtZBSA6nkncZE9VkXp2ovpOhseqth/V19O1KQShq2U27uEBtKBYvpNvVGQ7WhdBws0RjfEgs8a7G6mZNYEn6Lo5PYjvROjrhWqCSEN3U/8r2GrUoJtKl1qj7+cFgQNBaklBGg7hZ6oRRU9ZYKUL5wpMTV1KnkJES4Si9oofWQChgSozE2FRTQPLTe1tGyXsrojdHsDRJZtzsVA6xoF5KI6IEcIhrrxP2G2h6bvlGlyIn5fg2flxzB5YC/5foComn1sClhnpAYLBVJOoioGHsaixIG2eZHMoRvGQ0hy4OqEGE+IuZyYdVtA8iZQAwYJfZgCjm9Zw917dyc4PtLXe1SNx/a5eIStALT+H63MJkVcYa6bzKaKtwSznTHo6mhltoiJ6nCEYQZCa3OYBYq/UPLrwKTn9E4lUTsvyy2qRYi1VpIIjxFnNxRTbwg5YJxaNIqJOq1bVDPajNwAYtFOiGgBuvtlJ7b+vOR+foW00jt/N+olpRYM2tCFSU8rIgkGYv4upNRiyMspMEgG6sPCjuqN1AbddUxJzDddCMhwqlkEyzVMhlNtWD0ygNZ3bJj6Vk1e/xaTqvjJCQRvSVqCeI4uNGqgVAbRe8TBI7B9LAkXff2gsb39aKE25WDcK1h103Bixj9SKa9MVbMk6PGhhYbtrWD99cX/d7l6ck1mrBQKbmC6C0GqGSQL0kgs36sAro6kgeNsQBOkUyJYiHoKEY3ZAobiZjcuZ6JUi8cZu8FMI0G+E7DKy9Gs/rizWFGYz1nQStzYJsWT642MJ5+OqajsTE5aSh6wQbjK/ktxRH81B2DG9HY0PVvgb6VOnaldwCXgKeaDJ8vFfyGq/l4IfHQUzu5sVtb0ICM8S1lHKS2262QMHWJlCyZtfh2RFViAcdKzVHzn9MwJDEsrG5JA7uDYe8Nq7aAbsTJryQwpthM+sByf0c4cWsl3M0VWgRgKcmtJB6WtZruGb6nk3Ticw9nYRooyZZZxHXjVK4JNGKG0wd2h2ND4/QmJk+cJOUJE0TU4XWG7/V262J4adrV0rFTvSMjseRUKVygaOotANZtAr5IxDgRYwayQ+Ib4nZMB2hARyPoluYATWu1z1S9AYO1rSzQgEAvFQAkxywdjZVovWNpFBp4cD0gFiSjPeIkYFra0QnZyJRAi7RZnJXYNXqyMxhbrGNjEh8QOF6DpzkFuTCkANUydGlNAbU9U2SonOaolvGsMQJQEWAeGu7PM3jZMxJho74o6lNhNAwSOu7J2Ud20JilXLTqumKH/sSh6rZQeo5u5TUeWOqwpx2q2aavHAGRrnZS2gY/hWEc+xqONWGoXYqBpxVHuy+B/TgWvhyAcuZvy6/ntvVDACj2t6t2r07lm9AYJhqc7yChMX+hiA445lNkLko1e2P16VR/MT6i3D6hNIsh+t56J8E7ZE1kIZGET2is+ZQz7WowF4i4KpYRFF11ojJPCTSGZhoQPdTA4mOCIznW/TOiH0C+pRFR2Oy3U8HbkL6prV1H7bEMmdF1Pc01G5g0xsFNzO4iEo48Pc9RDFZXSJmUfRtMwZVlFAemuEKUV8cWOvQ4XUiWgBaSdTETssyHFhg20Tsfs80SKI0ljbJyYLRRe0CLfE5pVFsl08p7ob7gzL6s9DRwYeFBpD4BBYh1nOhGswXPNagWlhIlLN/jeAqGJE4DN2Mn+P49gA4NbKOYA7j9l0Vxu9ywgJVkIZpXKtXGHrQKl6k7ZcKalHJ6pRqQRxkPcMwbM6XqndFBLTy1HKgG2putRcfMEraZ7ZFXvaGzPTeD58ydDx+9nkcYfZjsM4dQi4AWuqgmcl5DKQ5nxTRSw1nBFmb1GWNhtc4CmhkQPaz5/QuMrWWY2QP82UPrc0hhgL2hNWQJGRHxmkSDiAU33niL+iHU8yO/nQLqG5AW5xmVdWc2lG4CDYsCuUrENp3Jb6JXlXoX60FPCA9ILDP5o/qYH0DFAGoPaUWQ4lm1yykMHKiMuZpYIejJQzMShR39PG6/iHXfD1WHSbjfaXV38npPcKOtj2rhCsYkTCOCJBY3GiW9ZBmVzdlLsDQdBp4IOBMwPXwDzSq6ImEaGPVXzRxueEfN6RFIY7VjwkGQchxMFfXBsMSZlJFvnRmklocpRHUIXfHo8j1sdwhXCpfbBFlr4/obY/iyYDzIFfYvhbixx4BryRRV5PEsEZsPIJ3hz39f2ukOzifey5LMvcKSrNMYda2x/YUSLHZF9zQZiwdEIVmK0uFs2prdomnP9cnZW1zhZfp05sIyFPL7C3bpcCqfrD8DBfvBnVGYlXtidyNOSGnnjupQthxnTGHEvOoN0Vs6xcsLqAP+hmG15N9sZi3oLYV5n3VAYX5JOBh+T+IzMaruBmQQfaI+mKbCPMWaMCoP7YgalEJPfJdv07GEv7X0mcZu49dhD6RKDiM80ovRBN+4RVNteaYxnmiLpvVCO7SBq2wbxrqkW4FPe249hG6QZr7iPlr3S6O/+O6KXNEXrRlkuTLtXzPDwcCupU3d4dSNwR0xc6VqALRPjqndGE5HY4nSxFOksUB3JIrUv9nLuzEDz5Fpybhz6nC+iM9ZfKjL2F1DnbX1kpOACovnOCBc52mAGE4WKQHPdFClZGhMhWR86qZJpgoq1QfsJtoonWiob5xa9TZid0caZKVNywPl+bkjxm7SxHo1LMuAFDOxQXrY1e72dopoHLAJOEl0PcnxcGhsbDkP+Sl8N06PefspDx+tg0QMh5r1a9CytMtg/H7TdL7jCo89TLpThkMfpSJGHh4xi5vZKWc3HMaAadPwhwVnUrndcxZn51vNojLbBZRZ9nSebz8EB6LbirhlVs44nXyAOiZs5i3jufaBCPlIH+vLdaICYlqK0DVB9XwrgBMlfq8GWYW0D7aMaBlynmusXS1nDjFjAayrZXvt7u2wZF8Gnd2BdZsdrbqTPMMWjHsChURbP1uLmjYm+N4ZeO2y4O+lWpabSqWK/iZwuWdWam+dIHHAp4n0tjct50oyTmMjl01JcCNBjPKFs0EuIE6sZH3Mgci7UeYNRIbEQwbCyvDZA1EqVVYNM9QKXsaMMubDgCxCm1oniKJQbitZRR7XVME19iAa+TtORwFURafKkrNoNY+D8la0Pwsj+WSYy08569UMUiWYS6oHUa097ifQzC4ECleCOohxFJOR1h4NCKUNmk0FuEIVQhaf83Ry6cBfEn6ZAS8PoQm4vz7t5YLrlUKudBPwPDQBj1I0tWclKnygsZAE6+3PciHbcoGofK9MJtKqA7UVMgHhirmGnIgxCm30fBY1htYFkaiTxbGw2GwrDNeZJf6FbfyIcHml4YHzwkbj9Eiw7yf+rnJhKDorhOiQKtpmh2oMXPM+czawRNpTl6qeaBtzpvrRSsik1lmiWko4vVWt3JBprqUfyXR+Czdk2kxudmc2InkK/tGqbmV+eGoGwQusviWcDqfa+ILjKXwXShsWJJb6YI2lfDwyfiM78TEnSNBRbM7f5BrGqRwzTuUUnNAgXaAp448zEIaYRjp2z0QSQOsKbhrbLinmNlEG7gIjOLjDWZLYiHgZiWtVXg2dmEHSAFcO2EEAJ52yRkt05Gxiem/JqbrFU7KhOq49bWBF0GTIYkdjJpWKqzufnRGzG6O1SszXDBAleWuUAHP8IAuCN05kS8bMimodjopXDKZqdEgszWC0HiYfDhRiJ7EgQcqJp4HPkhd1debJj572tWpej4RL5iNKXmZzcCUmIyYp7MxTrlhXjegYx6EY4xtL/HWscwY2BUkwBxEbUSGNNqyvewEX8As76Dbd2V4T/eP6tHfbbW1uIP1H1/zxT9vXS4vj/qyeBDSxkTGLduPd4t3Q0Gvx7x8fvXl33L/qHfQ/nFy/6x8c9/rdzd3+D0dn/d67g82XO643RxpSri/XZrYYrvImjJ6i7niejil1/Ht92nM2W1ArVO8zboDATfVGg/XEsY3lNNPUTcUYkeGQBNI4U027JqLFuP0KMswZcmGqZ6kkVysnh4yE0eNNnzVuCrWL2GBe0uvNkv4jmeojYtASmHPQLeZwHYMoLPZms6dKgqBwdSEbVe40mVo5zQpRWI0/UDm2FcubDQX7krNbGhIddJRrw2NeKLFfrLoQRnvof3/sbaDLH4963U1vsYOa19OE7P/vj71ayHBfiINVrF6Pkc0t6YWQFytf2vSTRSAwrA/sWFa3rmse9OrOZQVmYTajg1mpyi5+GBM4agqhSTGO7JRVYiY/opL5E8CcuLr0T//4oVbFI5wVrOe0D5JnPViTBtMSSpUIMMB+Dho9EWVsu/DY6mjpHA7lUV516CrBtBDK82lWwTczeKYCzy+BWh0lc7xYaHXjSyCW8fn8lbXY+FMssaYkaY1aaNHFtfV0hHLr9b+YBpXRp155ehKCeOpYfrFl3G3hxVRIMvF2/voF7NbzW91aEMbiTm5J5I6UbFS+RSFJiD6crNX5YuOr6DKCAFCSHSDxC6Eh5cLsR+DFqYJ/acBX4nsyRNjFksIJNTQkcWBVNKU8eeYuvS+H4A8buhhEzOwl4GRWNPW2Q9Q/qgrWEly0l+iwb9OkOcnk7VM0dK9Hb0kcgFc8dyats5JTsA5yCltBo7piEdEsaLRBtQqYSE4sUENfDclZRETD7FVJdsxVKbLObaIYTUNVxVVtqKxaEKjpqBSSCexZmaqv7Vs0RIzTEVVcnHAaBzTBke5mBmLfGySdhCPCI0dRu0W2vhtvBugp7BpQ5EAmjyfMOLUFo9Lc3wk73XyNS4sR7LXNzv13IlSldTp0aryZw37TFwaEavMNlrhyduQV6iKMUuy43ZOn0hvUxKoNcBRfyZgNs8emQosgVCGaIVEmDH5Fq1ZX8AWCNv1bEuTxdW/q0M1KZOjCHEG/pTjS239ACUL0y3Cd8sL4yGZDMdtvo+jnire0zDuoAuGwUvttxZYhhF5PbHAkjSs6uYrWyyVZHOVyq9ChA9cMxphbh/Z31mgkEJZaMikJAAeJEiaoPmM92kPf2T6ZkN8NlP/d+u5FmTTQjw+mXXFmkMuPwLyZLtKE8GYqCG9soAnBsYncnjrhZgQ4CnVwIQ4n1FImc4aaoyHgEGob67a2DxUikqC994Lw/OwuSCuXUoXlRA+VgkTDFnqvpO1dYfKb3Sz4MLVmbNY9q3iZxDI0RsJkrtDFzFHn8vqYR+oySkc03p9XCnM8ITKnZKRJwrgi+IEq+Xs2D8yRahie9cJ8faFjeiqkBADRR6khGN419EFThAqUWCysbQBycm+gNPblVKg0Cp5t+rEaRhZPJywVdoh0k8W3+/4GBi41QA1Ixt0w52UM74CWknAis9D+fI9QQ60aDTUYDbcGNJSSc1Aplq6hLetehLFuiHTQgJinCMc3NgrvhsQKAuR5rsAWpyFV661DW43BhtbDGjgNGxv5jljL8MgcVHL1gccBprfmu7Qmrpi/nlurZ8tHVZebiy6c2ReZXmLDh6/hAAKJQvRLGeAvahrZJRc+b+gTL+a6A51CqwKd/bxKccjYjTnobqxQVquonsUJzBibvQHmq2dykgwNGLuh9pQSYzf68P+syWca8jBR/GRbohObx9KetzAZobwZoZc+kFfgY1uodVcZEicsVSMbTZNrQI3d4Y8IhyFqI8hIUYgpKUI2FbNz1Fsdf/+SmsgtHcCh5Lu+o13NOToETQ2HTl2zAdOQvIyYjYLOTEY45CiTdGI3MnOdkTYrXw6Hyk54mF4RHLo+mZBO16MsMiUY4zgmkXZBak4pQLZHgo5MyUvCD6HcfnenoG0ZTsudn2mhQ/M2O9IPQa0mLJv4YWbZBsDksjCEM824LZk74TYgSsHhOJY0NjsJwSYEyNtCb/Tu0cZ+UxZugEMPjgpwge4YRLUTe77KLriAlyAzZ8476GN1hoMZZe2JtsJp91nQOdcJT8SYReEl4e5Q3Mt5Vf/uSKM4IWPt7m7HsoLuokdEBJMkO0sPJmxjt/ZKSXOyFlQvoUbKQNKeIdDEgG+xGY1KXDMEr1Sr+91Wp8BSHAc3TQzp8lwDNm2OzhHcUvMcmCEYs2zQ9JoR0uGQQBgYhwB9JR5NA2ox1hNPsYQBTmJBJoMoy2+TW+YhtlIvUbbgZYQDEIbmsijIWBgP21lPIX3kBqTxt+dhayqbRsWYpVGo2NLsZz2d3AerXYJXOLgB+hzPAFqku6uky+QYuEB/SC/z8BEw0LxxAIBalc3GZPZoeIu+o2Eul9li/VNSlY5ixt1M/zOML9DrYOkRhmoVY5zbt5yZc5uZ3NVDohaDO04lMZkKYYTnT0XNBBKisU0LSpXLzUgsUUTUzmytiPIZjc/TiRo6tcZ8UM3/HVpfM1jp8PscXpV9rwW03y1st8UinZIMJTS4yXdjCexNk6U+/N3rAvD24Y8owGpFGtI4tOhIk/kCjmdDEO8dOvzxnMnjmKWjsVlcjyFfqvYXQdwNmEFpcGOSO4RsglhMqulliFFPtgrLStsaHTmB5Hk6xheHSOh0LkStouqFPgoBp7mrW7/SAJS+0jN1Z9k7DVmoYJHWPgdT/zg0dhYYU3AEqdZ1ZA9T0kkNXwsdxFNf3Vl1aRO9FMgktLVLAQ3GPuw5wordOrEI6lzvRcuvYza1ImM+bWYdgjZhqzUbpNUyDWDQrAgwhYXuksVVU75nG6/D1YqHga/BZeLLgQd7JOayShQXYPuGQd/yb5MzL4qjw0ltSOCTMb7VioQDi62ZMidxXbdtngxj9BfmygHPsoYDCO3PjHyF1HV7NTJdRqJ1fdo7YrEk9/KtBlNA+Pq0Z/fhpgBcurO/DMQ8r8lIFA0C4LioVPcVsIqtVn4uZsYWfZXV5fHZhu+4zBIkmDsFL4/Pyu2YEDTwQKoCGvjjwHaBTQq68MDr4DXJdCYBszdXkuOGTEXOvbhhg0EMH7gCoDMYF2TCmYRo0yLf6f7l3IUQGDYPDYgvmY2IV2RRVMq+yzIyc6MGqwZvNpjKMMc8r1UEPFaCmxVmOGP4F43Iq17cQipu0B2B7OgDDKdErO6VywGSRbpkWLyh4uYDVD1UNZ3SVrm4WZHjdrcuEhWCm4nd25pMaDhG5D6JaEAlOj04soahTCFe66xZt6Dwak1z1USuD50i2xyboqc48BMLRZFnSHgvCGKRkomHrh66o5xkPth6UfNekJ82P1BOrH80vwG5T+xmIpdU1+ZlNbt4SM0hTCalCZFjkoqWn3A5G5vVsuYDbfQUhGt26ernxsaY5c5MqzodbNEmlxlU3Frp2+M4wdJoJ1AdauXyy5qBsCq+TmtaYa5hiQGu5z+cf8RZsoVawJVacAZ3pDfi2lwJTaxjfZBcZ8YckKFqTuve2u0Fhq0XtQ0eBDfF5srJr4ZRKkzcqE5PZ901uYxQQZBOUr3Chyl4SXInbVto3R7ercggu9PJpQbNJ/WFRi9Nm28VMsZEgvbRjmNyl+dxmhBtMyPBjUgnuTE2Sf7Vu8xumptgjaOro63No4aBegFukYQJQcGP7yWx0yUbG6hxdnCkzfZv3p+d/dxA6zFzzb9oVRCfjoiANW9ft1Yeau8Qmh+eW5hVEosbUcqCkWvrvHBGbX/7oY3Z0/ihbrayqZ4pU27tAC4F9hKheomMA6xdiSGWGCV4CgfYaFxAoKUNAbq6gUr1CRbtwYEt3//+dIZCykkgbUM4DnXeMJMLxE8PMCb5FAE8jWF9txnNwDBmHQvZadgNfey/234px+BQcDp2rukKXlaaoRIbZ4e+qwqY7I5YrR1OE+fvx4CDqJY2xhpDY0F0rmYa63S7NQ0esWRqcnUVY+6MwTJLmDyATSIks5Uub7SOS4FGyS01MbgCSQ75zIyhptzusSn7AUvCJ5jf7Hdar23b9o4E4rXjgA9tvlGnWRSYUZdftxVEW5CgQszl0HBJTPe7nU5mwzyIIpcfLbN+C4mnOrOdbmpor4KpsoibbGvG1g1IzSXKNZ0QR38/KWclfaQbqXUa+6eUNeIvNPPfjUEUWzmdbQIhA6POnr9mjlqPSGiNHvoERSoKRi9ImK/THlYIMS2WDx0w15dcL66yHArgK3RSABYpgUZwf0h2kETnT8LaJALjns/MUl7CzjC/0SlrVVuQA3i/03LLWf40lxtnhnCSEB2FY9d7u34attaqAGcR5E912pspa9/n5kHmlFDrE5XelSroO3Ssne6am+8zCJDuZIwFGhBIfpalaYZqIInvHerAnVBSkdDk9ZUwSSG1EoToWPPdhMZNz4mFELSjNiNV/HmG742YuCRcvzJHyQumBsB4QOSdQqNIEG08y6LU8k3QWP9xZYr7aV27HW/U/IaGRosKfPVs0RbxfX2Lm9udpRjFTKw6/VDPiflkxPe5nlUynE4lt1CDfs88/9H2dqdEULuW09/ntO7RFK2fHVbI1jN8r1Yy18czyAiyv9nZ3q2maSNLlwUpXMcQU5fF4nh5Pb0T8ZHNMtrK1su4kHmL6RTrYKLc8NPx2QSbt0S3ljUm0kTtKuw8063+ltLgJoLNdGQOrWlJZJUFd6obogz89KRKpKkF1MDLEpyBbuylPtNeTpOuFITeBLxzZh1v6CS/Da0pFVNkOWU7A5ilvNdixfcSQLrgLPX7htm9FpNpApUgjmtATJ7kMMu3DRLFSxOnFzmXPtkukmqPOTQAA47F2JqSi0xjcmtdAXbXzGQhMClXl2YckyEiS0TrLk7Qt+pUp3bTtFGk0VnJMgrH/kln6aXzh46bkXNJzCyJYxPBYfbAmV82u90NlFs1tcYEtFvnfHE3bCxDrpPY3bSQW297NzRxFv2YxU2TERYMJmmsvmg9FUvcNMJA3/Zl2jUv14S206ITicYkSozK6VVTa7c76RkwzlPIe4ClTwhgHQPYiDIqkJBpcKN9z7BSgXYPkHAqmerCOYuvMsTLgaPlayt0ti5ItqOPhvu9QfZmwioSm0/ufgtRCCbOtphXauTcyuAaznjErRngZHFzBnQzzxxcRkJBVmvGtCaQGR2EodkDzGzX5qGwbJ5ny/WOfxydyqq96UE4G5FzcudIoG1fOoe91dPNKGc3yBU2cZkjLWb5nH1+EukNH5bnr7RZD0dwg6CTexoFCCcx8N8yblYB7XCLsITthCCxoDorfUBizCnbABFinLwYiQmOIpcaDqx5OlzdQr4YooClXJjs7MUaE6x2iSOOQ5JDQI45S0fjJM3bJLudSZWYPCd3RoXQB9dV/1yAUDdn2LtISIxAOjR7GlW9YanMs5hrJCdZFBi9OyWyKtLgLL//ysx4xrRnCewUCHdoP7Telp2OqFAjMoMeWPhONRyr0pQnod0uq3WqtC2ERIH6liMrHMotXqua11jcHNuCfk+NJfEU7prCEY6DoiFRbcE1OTg4fSAQrjYkW5U2cFrnjCWnWe2CByjym3QtHZoXlRbpyyxXpgkT05Pe6HQaJCcJ49IkIivBvYKv7+GjH8Bk4HrcNjE6pG/+NtnNvHZmNnCG762V2in9Ly18tz8H5UUjDYeCo8jmYfHhvmNCvlesnQvZcmAtVBhIMSZhqDUsXczlBtYRZyZXm8kueWdYTAep6ZRb3t6YDYfQXy9yBkoqfm2qLyT0sjHpwGqQh/mPotSjnsGyti+XnNyCb8nFbJl7eEB71Xnd1YrLbs2teF7st7NeSJ3F2AQ7upuShtyeOahC6Qc1Zy6BVi4juieGYK3xjUnWKldQST18M6e8354elTN8b1JS5fZNJx7jaaO0SbSHOBEs5YG97Uiy7CKqCn7UZeHqIM2W9TxJ8q6mLN+7zuEOCou+oaPUzkEqmU7M3lPfq4RqAbq7CgXpu+5gazPM3TFYZhrVzHsoDs2YVPBVrdn5awYBIl1tKnrglDsqXMd0o1bbz1v6/OYLOeizkfO1UtuyucpMoPUsj9dfsgxAL54MJ3MtWjVWEwE5EcGy9he1l3w6PM50U+ZGtgIeAxyHdzSU4y+ByaFtzGzbu2V8ihcWGJZxc6C+FWNtsNcSdDd3/Th+l1zazNk0Lz5y+3xZW54KNOJYahXQmGTNjYj2FkCs9g9wC5CrOSJZOhRfZMECYXd5PilBjBqBb8mgDzo4zJxOK/UmWuIbYvJNKIq5RRVkaQTOUI4lGZmjkWXxl69BeOVqDOQJcKRDydVuj9wVaLRhAoB1iktNNhxAOnKIcXtExN/pRqxs9TD2TP5q8BXffaByfBIbhIzj/q6+O6HpzlLoziYzML9CwiKsYwD80GYf2YtU/gtge5HK2egeXb7/imgeXb6fjd6Y4MTalL4emmeAwGxM8w7Er4frG8BjHsZazBqdwd3ykm04fefR2eGLDYRvGegWg1zFzOn/iKwLLWj15IzG1w4n31uU33iZm7lyJ7Xz+yTulZgQya371nt/pl9Xb2Tvy3kIdTADXCMdZ+Ea9qhlDCeHJky6u2AtrPzF7gKNmZBNyZrqX3ex04CYVcoNCGyQx3Q0bppts4EX0fjGmAO8zmSJj7w7G7JjPVfZZcYuJelvKUkJWPmLwGyS0b+rEuZe3mzptxnyMlOLT+ws1FpbN71rlGnsLnlzvTFhIBOqjdvZ1bB3kHXb3aJZTq7u4wttFtP3GZtAKfgju/cPzob7JgBjhriyX7SzJLtRsAApg+CoWISgqHcSnx1mlSvu2rR7yaoMdULq/G3UpMG+ISQ5UL+K54+2CnPkAxn0WHBDSqcOvZtEszIHlyfu+qR8YqE7W8akhayaL1mYyMmFixShsU2oceTyBIH1PWsV8hbkWzlPJycsuwC91IBPH8Unh27+PaCxylmz659ALN6kbK6YUyIxg+zfviyUSl4kHHw+CSNScedyblFxmyD6O9GnUCW5z1jWJFFJMLeuoJndUxstci/fql27mcbbuy9f7RTi4bQcrGMTL+mIL0gJBNxdu4whBspJ7AXf+SwCVrDYXMN9GaWjEYD/35/O0A9HFjKCMAGeeW0iNlKf7bnUHAar6NfbyQ9HpuFTqJnZ2gpHcTWXfCADBBbEB3XWXeQAIKrOWOly5hppnWHe3k0MFlC7aPaI1NGMNsIpu3MPNG/jFTGb3uwlGG61WmHv/LqzFid9DeZwSAPg/0Eqwc480He0Bql/PUEx4LXl91IjboI2cuNZEUzpJZatzUrnGrVFISNBPoMWRDT39IlDKK+vlneOOvNpvw7Y4tcSA4B8otuUj1wlVzx3/XSembxz3bksYKbDzgYMlmTVMzwis0L/daRpS+iSrSgctN4MTv3aVTIiJlJOEZjktLxQclvt0/WZmGyZg/uPQn2HMvf8l5mb9lcdba71Wj8mX3HIy7NDzR/Q4Bm+d/LkUFsFtrY3u69eZfv3nhFeEKmKwGHQQlpBXSQYz+3tobp2VHjJZgfpcEi48y/ZwI8oFSYpiQl10Z6wiI08b8+IsdCmb8Bwbx71j8EM6Mieu5EMgTKMIc+1xLGkODKhSQagFwFi3FLa3mjNx1bzzu7H8MwwGREkQ932to4RhPCnGXGC4cDwQx9Co4C0JsTibLBfpD+4IoOHkv9aJ8G0ICAmgZOmCXsImT0n1sTgrFZ7HqJUVmzKK5UOjSEwRjwpKRQSBwqHGnIohWsCEfNZvI7tSRa7px2F2O+x6sHMtg6VjlrQj4EH3MHuHW+BX0VXLLgRbw6z5SwnAoUtVpj7Oo7U1nUB4zpMIyT3ufO5q2hdi50TOL0q+fQkfIGaf0Pr8OOUjTYQGw4FkS9K7GJagCvamoZv1KwZECHzs0bqi4OD7GbYOXOIjmI45hFLlDBug2FcF+xmAEs8wBCHbRIf66gU9P9u/nAIiWjYhEAYiSgvZGYl7Xb+y4LO79AzFT0bTd3jPvTYhd06xtFx3aVBkyks9iXWMMBgZh6CkDJBvNs1JYWQbntQVwZQA6y2O6936guxyVsaScIPKVxn/yOZqk1zdfE4nYDKJvab3foib2lExEkMJTu1+E30dDPlumeH+5svd0oBZcbvKxmiE9CQIG7tV5ZCSj59FkHtGIMxJbfmpKeNVDelzvA9HO77gKk865Egc3j1xuZArY5YUUw4FFNIDsdi14hZJ/JRQi2/hd40DvwoEm/dACY07uOQ8g23BsOxIu3syXjWxMdXKFud1uuXwxaiLdJCr18CowIErxbCEk2YkLlbkGMmx/oOuMguKwOSj+3EMrt3AKKaMES1WZT1R7upHKZRZDKm547O6iiKlMc2PgVMOxMWQsquNfXmIo6mZyzMbQPXTIK5DZcmzkNUjFMZsjuX7+cnSKujc8l4ckMpLyZYtAPKbxetk/sgSgW99e6pugE/YmYZ6rRev85b5cQNGnJCzKhA5jIsIXaJ35QdCG9UeSr84GdFHG3CKzgd4KL/+4CQkOTrc6JoZu70lAxiqgCKUvqJD4sqhUXhNA8vnc9Au3Ihhx4s1jr60V0dOmRcjh3ZdFAxNCk8Q50dWpNOyF6mZGKr9TZjkmAdU6+YQ6/ZcBwHsMCRYBYVex7Zd2vfUZPlzTsRZHECBUFnf4tD/cs0bPD0z1CAYC+M9AfM4+rR/uAZTewp6SzLvQ67iRhLgFqgkw0VQ2BFTUVaOsHZERH7eR2mpju/BzXsBY6SISIk1sFTCgYNSSypnL74n5J+s1HRtJ56MZMOT5PiqwaX4hUn1Z3xAHgfUpFCsAKNQ53d0cDChbzFhHPG9SpPdFb3DejaEFMeQdzpxFyvoVbeny57appnh7+B12V2ygFhsG7q5GA6XTuEooOLyXwAQ3gEV+cZKN3NV61Oq9PqZtc8UGFOfAhi5JQSZ95JcX2IVrvwPGGTDZAcE8rtEMFd/CJraG+ru9vdAJ4E5mQxyeXvy7r3K8s3pmn102VP+Gc7Inqjb7/ZQFOWWqlmT2i6OWhycELUJMyw4mADY5yacc4ZGN6BzDDnHfzgGhvFE5N7iUaYD+BKNB0OQ1m8UTRgOaUvDkhFeYgENgeclEjGwY1ajeNwA0nGsgtLR4G/ZIwJ5jkVEWJCQdhJY7eAy1Nx+TZlB8h1Sh+R1keijSZpFskEB1SaDcooUJrANZ2QghktZ8qB0At9aFNfCeDfJoZc/F+WDpGEmc05X9mzQIOC8CIbsYNUMjA2HJnCuaGDtHTmwKHGStsQcxcrDbzIDh1mpBNZtda924TacGFb9nsWCnBysFC+QKL8rcdVlHo06vg3KdUSqeJCJZvgVyxDM0jjVksUfdLewW/5ZvvcZUtOw7S3KnEcC7NOzryBQWYFbeCwPrBvkkHMCgj06rYCBndQYsl4i06SqHUSn5HJ9RzoKyvNZnMFJ/QnwgVl8R7CSSLat92VGxqHe+gNSSI2Vdrvio143ltBELGxZ4IjVhCK8IBEQudlHWJJzlgaRlmBjx9dno+G+tzS5Ruohf6ALV0s0Tb69GlF7ZoUFOOtEXtIbTmsM1DD19khrogePvUZEvcwkxYWssieevgshpFXLYfWjr4VRpJJErm7VH1KgOku19yDSbBrb6CxZFCPSRFBuAe/WaS/fegEj8ge+vhRzzB7sQ68bnEyokLyKfr06eNHyAkth6jxX6KtkIB3+hKcjx/rbuQBOOgPZ5Zp6PcN9OnT3pxa13jkV9xsvWp1Gqa7Hu6XaaSziU+hF/kOJO4jytWE3K5xuOe9UjRqD2jcHmAxLrxvBoUXDVXQBPpAClAcqSW+GQvR8IqS+NZvImFcimKbbrQuGZd7aGfnZWduie7sErud3Tkwdju7PoxbFqUTxX9xGT+wSF5iOd5DLicGi4ftrNuQ1ixXKz/bm+q7KBQQ6UADnQWnsnWXNLymtdL3jx+beeaec2tUdeulnCAze0xMDLKPg3dh1HykYhaSnhFTWS3/7R5SIkKyn/EkqpQJD2hVsshlkHaVvJd7j98kHg5pTKU3Qe2bx2hMiwEnKHpqDZBe34pfatrcmdGm8Wof6Ji2c2ADBcTIf9QoF9Di09Tnak/BpZVhB9EdntrZoidmrRwvzSytNpzhJD+LS3XoaOVzZkcRizy3CyBlHgP97ry2VomqpUZy01qtjdktcO6cZkBaJrNifi5nBzl/AoJCFuY8gpB22g1eLS2okDQeQX1vcbLLGiDZ8NtWS6R3uZx+IPn3G8r30Md8SU2AonrlNCvjTvySapVbsuxoNMZSJk31Wi90SX7VkpiPiCwsZebSPvJbrdhRanPjnIVwJ2gDNU698C1Hz9h8nzVCCjsLx9Yr8JZNN7SHro8uiz0TVV3rVnWt+zW6Jh7at0k8KvXMqQl+z9zLWaDKVHIKRQFWtxKWVsjruwpEgy6tzLvy0Q/0O7nUlfLv9lCjvqVS9cZKgail7cLn7A227fzOC7ACUhCsEZwe+sLsQQKhqSE1ZSS+iGyQ0bLzp5oz/DnyZPQ3I6xHAsfhzOVkPWZyoVXhhYKpR+eyatUpDmTt0Jn17rEHDQcBEeKMhVaraM6Yh1lhPRdsZL+panLYO/XExJfMmtkQRwKwZs4A4cW0ZKLOSNpGs7F4Pf+NXt4bjZXSylwuNaML1bg5gZFjrdJf3/w7P4oD21bxFW3OItKa4kn0mG10Op3OzvY2/NvpdIr/dnd2dr7pbm91up2tnc7mzjed7nanu/kN6jwmEnVPqlT5bzqf3Vaxc3+SZxUdsWTK4YzCZmdzE/10doc52UAncdBaWV1ZRac0ILEgoXc2/wDsk/bLBjLSEW22OmhdFWiYT40X36+sgv9jgqfgF4JLAOESMgrBkXA/NuRdnyQRxfaEKTRjgLRWVtHPBgQbwAUb+SRnptz/x96T9rZxJPudv6KfZMBA1rwpyqKxeJAlJSEiS4Iob16wWBBDskXNiuQwM0MdcfLfH/quPubiZQee+WCLfVTfVdV1NfJi2mHCdfXq9efnZyFIDcJpfcYKRfXL/tnF1eCi2qo1aPHPixmOIvWm8Eh7lmrmPdMH56YhZhJmX0YDfIei4D4ms1U5RBM/ikN/tIq1iRI9Yy9+yALBAnkLdHA6QP3BAfp4OugP3lUO0a/9u5+vP9+hX09vb0+v7voXA3R9i86ur877d/3rqwG6/hGdXv2Gfulfnb8TbzHhFxoGhnTSJ1OIJ2S+BhhrHRBhFqTFysxbTFfeFKNp8IRDqttZ4pDa3PMA/pVDFs2EyxasQdUqFYPPWwaTAQ+nzK7GSdxQOPLGxptjj++jmh8okTR9OAlS2iSqSjBWfpoqKDagFUsvjF8JBSH5ISEbjEHylj4LHd1jpPLfb5kfw9v/cEoqCKvKnoho0uIpRl72CYcjUY6VXUXYgEOdIHsI/Tuha/+pAB53e+ffgf9HPo0ouz0ykIX/j7sdA/8ftTudEv/v4yvxf4n/v1X8/5EholxkQJfYbpEgBDN8i+/pPYwThJT+VxAC5CsLdrQa/RePxYVckw+IcShRcBIQUYLatrFit5g+71uTfvXsPuOkHgb+59PIvb23RAKy8H+nZeL/buOoW+L/fXwl/i/x/97xvykNFdjuayL6nXDX3/5n4n8/xlQe7M3q90G4iPFisjEVSMf/zU7zqG3g/+Nmq8T/e/lK/F/i/w3wv0shcB8GDG8IxCxVNmZJgm1uKLJhvry1eCZibySSjDNhL+HQwyym/uKFWktUqeq1QrMR1QjSPGqvVJN2fuhPxDIReqaPXQyXYTDGUYTpm1rBB6bw8ieoHs+XdQZi6U8+MCtQ7onxRRoEcCAwHgFqNlodBucvVov0DH2pVJQZg48X8XAUTF6HpGfDpRc/sAbNrA+yEvXsN4urxA/ASjGKx1PfLAqTVeHVc2QXVYmqYOQAGSl4CuAyikPszfnDsWCqEDfDwSF682XQv7u4ub69O70cDi5u/3VxO/z5enD3V+99p9NWjf6l4DrBMRN6RCqhKJp9cLQ1pN4GaBaMvdlDEMWgq7RQNBvC56dQ/ckL6zN/pNFFUqLO4NXGYQzbMQAMH/FrDiCP+PWD3Q+29rA7yYDGntkTBoS+k/7KYfFXaLyZY9RCqRuhu8vBU7PWcnTIXz7gMEJv/8e7+nx52Xu8ODv/+R+nF4Ofzj714N+3g1PxpywjC4jc3lu7hWWI73E45Esl2gsWdkkeXWPInHLZAy29weCy12zMjdGFQRAjhOqrKKzTguwY1x/iubFFqNsp/69GstWfZkGG1+o4HnNoc3+OqQ1EZDQ//cNfWkMgicO5vxjO8GIaP6Bmo9FwlCDn2cfUaYRSpkXAXHDJH/wROf6Sm7eKHxwAaIdouJD6ckYfJid/jqNIUDiyH+r/jQg9ggnek8dM87XklyrIoJBe5jO9hP37H2HEe6AqG1NEXb0J+1A3DjSihvOvQ8IqROjNKvTpP3VUV2ukj/qvBMj/hFUcrXiTyfABe4TCUZ/f6lmwiMNghg7EVL+Ts/8OzVdRXA3xkzfzJ16MD/L1oe4tfdcIBTJfelFEiUPUq3PMQKsYswWrmPgqBUGw94YtJJECjSKvTIgW7mJfciM4FjP9cxDF6A1DwzmK/1/1Fnuzav8GvWFBtYbeZBLmrPpjED574QRPyF/oDStBVv1leC+yyF8pk82CTLDYqvfJzXILC7N4vm4Obqpnl/2Lq7vq2cXtHXoDCAGOxt4ST+gK5Ry0gjY4v9KARcPJojCQf13c9n/8TYPD6Iu5//W/TOudNd1QBFeZIhqQRb4hV5TkPu3MGaXQNBR2RwHrIL5dOaSoi4TpkgKwUVWUSnJQ0aEYLipU6hFvy0fF8hZZ69oDD5PtBtLptPUGbGPlbP8SaPy0pb7mdUhJIigGNmKbDa6zVUiHq3gxeseb1B0XPmcb1qVRLyS9XtLhuWbU8CUSJ8h929Ga5Y8fw+EzjqAiWkvwAJCbfc/OKLLdfbqjyEZLh5QUh5QNznWiL0vKucxyJUmsWs0+j6m+Mgn1NnLS2CnTAaiF7qohKRofwtJE4U7r/zTPBnU8c/k2ZHg3aEgGujY4drZpLW45byQMVpE5bbAq+e8w2ESnCb2L3AbZ6TMhS67rNZEMwPKbSDbaL87FOsz2v7aqo/wcX4r+bxlE8TTEUZVtpfXVgBn2H8fdpqH/azU6nXap/9vHV+r/Sv3flvV/Am9A/d+6sUc4qDSvKlHkGxL6JPdpdxFIikxD8Rgkah3Et7MoJHL3WHFIeE5iJBKtpiHoabZr7W2JeRLu9TfXg7ufbi8Gw8+Di1vXdT6tz6sIh+hP+g6ZIc2xwN+cDga/Xt+eF21i6UXRcxBOcjZz/rFoA5ORG3SeICpHnXZrA2mS6MPvs5RgI4KfcZSQUh6QlxxmQQx43xE/RLt7jfkhGi2FLEWifvDrHPRBzoh5YeJac5smhKCoWgLgLECbh9YQWyIluAZs2wivUd0kasYuGQKXQCYeL6uS8GgiCoCxoIgCJKdEmpAneRsiCg0fbUtEoXcxTUQhS64d2CERQAERRXGeC4goqusHFHCehS2EFDAO7/Z3OwwrgPimT1ucbUYWEDBhbIH0veV24a+mxRdIrpoSYaBaOMRAVg9BlIGqCi5Q/UbiDKTIf2zt05qSoAz776PGkeH/02q1O6X//16+Uv5Tyn+2LP+xVIuby4GcivBEDyG78DckG8rTu51JidacmsLyokTDBbRDyZG979LMhli3kmRJblh/d+OhPAONZ5F+hUDbNTPKEp4l2cLk6bvgxahlaU5h2vXtxs3R+crXXAHRYGJzn/OLCnPL8BIbO0+Q6W0qlUyezFxSSmBD1b/q3/VPL4en55/6Vxt3w5vM/cXafSDLu3EXVhEOC/fg8vrs9PL89O50+PF0cHHed+4yp/EdoR311ZLcvfEkvZG7y8Hw4ur04+XF2oMD2Mo5tA2RXkKnuZ3d2cX6h53RizOcddTdLf9y8dtmDf+CXwu1y2y2Nxoxs/EuPGLe8gYjZg0XHvHpZqP1Co90E9ohyGyhEV5/+nR9Nbw6/XSxQbNn9AEf6vaedAItor2JcW8SfkFSMePM3AL3sxuzY3N+EjUnVl/3rC6y52qPaiObsJbqozWCxjuOxr4DqVsrmaz1cUgMxQC2E2d9K8hhl0bMG1kb713KkaTwctyTNdXXJqbI9iJtQwXmxLWbmiZnToPbSHmtW/bfccISdYfuLqfpeawa6+oSswEV0CluIqsz4savpVxMR72bKxmz8HUG+oFd21VYc5uP2J4a0r5qZYQ6tysUDXqeA8IWw5/n7W8ZCP27/Uz979ILH9m/1blHXzfdPAZkhv1/97h5bMZ/7DbK+L97+Ur9b6n/3bb+l2CPbSh9ARZK40RgsQ0UvV++yJtsM2FMHCOKOoISb1E5nDqW3amFi05hcYWwvpbi25kqGC6XrQYGnUlUAFsQdqr8TboW6duOc7paTYP/VeBC+gTgm0f8+g69efJmqPfPnPBNSeqXLxQMmSkgNqMw5Y5oduwBOUJMOBLzeAdkP4963Dg+Ti/RbXS7mVJLOB/7FtfCtvcqqoUNl2Jap5h2HWnePsiX9aDbMx5VVz5txZZvZT8iKMAATJ0ERp44CIYn6mCqZE5TYcmzqb04xxIZRT2brQioPqk1Fn/30FWwsIns5hS1s64El7bG9Jff25qnyjA13Lb5+5oWnk58YHP7OzGrO0rWebivnbqBiCbl/s+i2e7h/t9uNuz7/3F5/9/HV97/y/v/ru//ZuDubL0EwD/q8fN0aspK56emokXODg5pYDeC63roT9oEATicBxPMbMARdQ0eeREWdxUeIsqL8fB+Fjzz1FWEQ5YqtGZeFD1PtKSHIIp7qNk6rjVqjVpTUyq22w1xTZl7LzCWeA81Gw2p1vBmeBj7cxysSKWGxa0UFrTwCdz2PKcKWloJm4eTnl0LWpLGsmNBS4EpXFPQItdSfLsVtPDlShC0sNx0QQuE8PUELXLbOQUhW5C0pDewa0lLXis+arG3DAP6OFmdcogEO9a1Kom4Oiou22nmE1Hw2fsqchne9v7lMrzhUi6zjvlc6uZMjSbpYkHWFghwMM2dEddEQy4C1bLdajqMlkByfjMf7WTIa2/xS29ROlwGF9zaR7F77QHP5v50EYR4F21k3P8blv93s9Uu7//7+Q7JzZveC2pTP678QGVBP1R+qIu/yL//W6n8gF92sjfK7+t+9PxTB5KoKmhxFb948+UmET/1L+v9v07DfP/vqNs4Ks//Pj7bEPTk5OSkAl4UtvLGD17IGTgq06C/AVNU69aa1Xs8mY3n1adGrVlrVOhLm/1JD1EA4r5JLRuX4BbXv78K4psQRwQfWTwnqkhh/qsA8YhfK8CclvRoFuGKH8V+QLgNbl4r0q0XSh1lOHfDFCzz1+j3Gf1r+Ro/BAv6Jxn1KPDCCf0ltT1VGC68alvIUigqflWVMT/0r4fJPUuiQcxZydUs8kilyiHyF6QO7dKhap1H7TlE3mIRxOL+wZIeAmkDe6gCgpFVICtQkzBq4qiPgzln/w7Rkl0Dee14FsmWqpoXxPy1Gs+iKkuStUHTolJ2w4d8GmVLGQNYBARO8GiBoXOaFwpjYE0QYhU3mV8RdBvCrhzyTcTWkYtk7vGEXB7xxPPrLLsq9gUvc+dNe4idqpC97E3zGFsu1Gg06SGOl1dSb9ZuNE6Oafo0XI719BZNNwzq2QQ67mnUAPkQCEAQ/a1duVmf4E2YTp28pJJf/KRdXYmzRhOhTbtsC1oq99ABmxmWpSzAe+gWe5NfQz/Gn7zFK6vo/4F7qPmTDxVwc2/hTXEoFmwchDgaLnE4XDD5bpdnLFg0rJZjK5l1Wg27DvnJFHj8ZtOra7a0PakqPUST0H/C4c9UEmwl3dG15ckE937yXm5xHPo4MkoP2KU4VFBGs2D8+IkN2M5dvtIO3bCNyNaRYB8xNWQn8xHS5HqdpNCEk0ZDjJlgCPpcn//CVjb0RiM/nv8Oz0dPpoqpWUyH7BLaPOoet8DweggkGBL0QyQCl8rEQ44d9dYkxtTbkqpv0VS3e9Rg+5cgWx2GwL/iNHEYJ205dHKWZGqzQU61jpGdJxuSjDzHGpo7sBlhKIaGJ6jokyLSKhKJJvVCkqr0LhiiNHoWRHxUfjxzHVrWXMqhvV6MsXFoK4eM5upTKcgw7Pf79XCQv4TgNJ3LoVLxIDydhsFsNpxj6gZj7EpzTw4n+Gn9eaLdKThNFQI7Jtti9olU74thNc2cGznEhpl17hyuBeGzHLoN3JqGJu0bpSKLKelV86RVa3bfU0WXzFCU6Egl9sRE30oGUZGJP4LgEeMlDhNIMIdRleUor1EVyYQs91pNLuSirQy98Uy0QPtMMXYFIHFets824QE8SpoNKSxH9+aBdqhEvh5USqeokn0yHZgo7VmuCMwDkDTH84By0K2f/ANZmSpsi1e1j5HjIJlHyY2sBDCxwCfHjAVhIr7sGeXKosIz2lpnRlvOaenkmdGOs+r7rc+oos6MNlNKzMs6MPyDNwmCZVWUhAXpNLZqjVqjykq1ase1Dn3T9f3Od4Ekny6EmIX4TLbORqH8/sQQeL4pEiW/5hTtcDIAd+OcBcHoFOAE8lBXF/fiumXYN5V2wk3lPaej4wdvMVXrCghLU5hQMLCyaqMNWDaVygCGwSrGw5jcRyRv3Gw0Go0eUvfgMHh57SHtKstNNxRNg0/dajwjlWdpmYB1JJnvVUuEVN3PguesxtLaOk5rq0UpnOKa3ddPwUDrx+G41tjeZnAzRHnZHnuzgz7be6oj0yM94yRxYy5Xo5k/vvwoNpohHCqwF5dyxwncNwnmnr9grFItCKfWRuTlKK+iyVO0yw0ybzIcCUSM1et2j5rahjYgWV2FnW02jiyANPnEPBUHB3RHcXmWtqeAkEtDL+1ae72NxHjvyI/xkAkiDRbczJmM7LS1OPOiu7MCpEdJp8z1dq0xU1xqurVDp0ixc98T3tcQVubqvYxRsq2+51oTs6vFMQgME2nsVhjSUcbyV8nnjo0FwzIm594kblYt4iC84cM4gD36SwroWCC6lLBW/AH52phLBmUQuRx1HvErkMhmNaRelgeVMhpSb8ezOl5mI55sQMX06aG3cbjCb0Wy9kQkTVGhzrgao8mj6Wly2a+tgvmqn0v/ty29n/jS9X/tVvfY9P/vNI+bpf5vHx9TszEDXSF0+fIFKZtQYInJfXOWht0lMLUEhaEu70DF/mFmZ25Ls5xWZi1kPqeg9Iu0P+wHrRB3O+hPFAeDOPQXU1KW5qqIKeSmTEGAzkWxH5A0W4tIamAZvlSMlbKGSH/WAcCzlI60G6mayLXa4VpC2nOlMFT5UgbN5s/UI6qCQN8l7O+gCizBuLLr9L+CcKnETiRz8d2aoOKZBMT0kwXgaD+0GWJ0SVixAX1kOTdMV8oN8YFirJyZGruaSdxIbtjl3MjHzxS/ZtAAcffh2EhXun+fU+d+IUgzcDGpBMiD1JeSCkp2lT1LhexPpfdFwtiWAnrwIqllR8LCV/R45I0f6doA1x3m3HGAajzTnhYGTaQrDxTuJwLT6c1RZt15Uxua5TMBJP2F1k2ZM1OTZTBp8tIsnZB1kRKpA1NEKV1oSUrBFFHKjF5IyrmiHCaaytfsdHuaXM4ReVwjcuxy2/Uh2/EhG6zp2JDh1pAEEJiWAObp6srFPtHyuqiB1tHi2srF0KQPdBlgED34Pi0/PWIgQCJBqoHAhaCSJquQdZm8gjbm/6GVb/7kH9iTqJu5iHyX8QuyDFlIK3qa7lTDR03/zFxNySWwYXBGwb2SmRh6cq8QtDRRQdBERXDwRreheQq/EsiEzCFAco40QxPhuEnxQBCBUKnK2oTkiV+gQzJvmRSoQMN13G5GojrNumbb3RA+qySdPkMM5lFIn9gk8jcpsrpPdUsS+Sgjm+SOA1WGQLJG14E+QyDYYcZcCvKp3XXchE/m27Qv8f60Lkkz6A9PVSY+YhkWAL/nWgg+Vu3iYo2U2+SkDBNegtYdoxS2UxTGfuTGvPkQL0SKYlDZmLcw6s3Gvd8rxTWOmLLacuw6ZuGUsuloga2eq3TGrlw+f8mncQlnii+UQDs6vQDl2u1GVxZTVnOkqPgFi0PzsgOI8gDV0W4vsTqQCbjPKD6c4KeDPaKfvTJ54pghaCmnllAa29nrBsztHMwaywTrcavJgEFCEhsNrPLA7pOpyduQD0eY3DlwhrhfJuIMIJFSfv3MIk+kQjs+iVdo2HgtNiQLCq+jGKsUQzWioBbOwh0Y1MBwiUEWsy6y+Ty59xS0cB/xCXcRilBRBximK5NGGBiY7DBmoShSoV2jvsO0oBiJO8wMfJG0w5wRUfRpSw4usZ0dtqfwC/uItLCLoApqh0GP+KI7DHBU6s5r4UaSlYYa4dVZtxrVuCuRpbNZJqOllwIcl8ZGiUK73h6yoZ1uENnKjraIGkXmJjFDIWozrb/Y4uBtZGknk+Nic2SNJDkX6JnJ/qhpy3XlAt2QvJBqPoUp0s18tT0tstL3tF4qYU/LQrve07Khne5p2cqO9rQaRY497diqsn7urSprrLFV1WysvVVV8zn594omDrOwOs1LQ+uaYG1Thcr3evFdX/+TUwG0BQ2QGrQwNlbXV2h8rF3j5Q5VVvCiwzeWLFXXVSWGz0UOQ3lWxBn0DBSGKEMGNUOJUc2SUYxxgKBA3lZg0sy0I6RL93d+hsojlE+Fatjoi2LRNo6QMOkXwhHNwj+/0VXqxK5zTJO0McJNgJIY+rcp7KoF4TTpEvHVz+uWBGwuKr6eYF82WFjeRiOrKQcHF8Lh2akoh5cpkc7ekI5xY1obibgVo+nauMmIH92R3HjfgjjafQhcUw09WFy6Up6dqirVbLvKLf832vJGFyrI6RvkktebfjlponujbLlP/nb75BvQoyXgtCTKrrt9Mb5LpWiWXJKqG1VvAMumUmDVo067ZVU7H+mVzkdalGzlEGbV/CwJEEzJW/tGJ1RGahYUwzGNLgtMgvVphqbZ1SvCFFiPpCuFKPBsE8pL9hvUyOPyZkCk3mgK4C9Yc1TJ4w8nAUKfOGo4J3/nA6m85QyQso/yZyGAWh890D+vQN88rV/Q0Y6iFvWYPgDHXPBgJXVGxCPnoPj7TqcNC0MPPV5FJZkb1Om+52bcrdg4DD9piaSkI1KOVVLgx4TQOVZ5kWXVUYfZSrb7oh0fZxYqwx6XX/mV3ybf/wcAAP//KlQBTwDOAgA=` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} +func getFATEExchange161WithManagerChartArchiveContent() []byte { + base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOx9fXPbNtJ4/+anwE/O7/FdpqJISZFtZW5uXMeXepo4HtttpnNzo4FJSEJNkSwA2lFcf/dnAPAFAF9EWbLT3iP+kVh42V0Ai8VidwFMIUNd9MWbw3CGeidzSJi9hIvguy1+juM4o+FQ/O84jvm/0x8efOcOB47rDEbucPCd4w5Go9F3wNkmEXVfQhkk3zkb4zIb9xf5YIx/QYTiKByDO9eCcZz/zLjizrVHtgv+B0yRH3iLO8d2bcfyEfUIjpkoegx+RMECeJx9wDQigLNVDgDA0AfTRdBdwBDOELFCuEBjoLGedVdQwdF1JbJuiu1bd9N/7afP/zsYJIhuWwCsmP9v3P4bY/4P3f5wN/9f4oshYcszfwxcS/x5XjE1LbyAMzS2ACBohikjyzGwAIiTILiIAuwtx+Bseh6xC4IoCpkFgKhwkQTBFfIIYnQMrD0AukBMfMuKI/8KeQnBbJkCsABAIbwJkM+RBxRZVswFAmUo9FBV9iLykwBRnhXOcPiF/wEADr0g8dEYMJIgmSJo57ILEciQD3FPlC8yr+FsDKTQIShAkMqKbBmjMTgJEsoQObsQaXtgzlh8HvnoIiJsDMDAcZxBmjUjsWdkHaZZQQT9H2AAQ4+DEr3Hk8PIR1coQB6LSJ7IooBTiqOQjtMkOJ3iELNl9ptECUMTxjsEYAoSinzAIuBF4RTPEoIAmyPgScoBDqcRWQiAIJoCGASAjzRGFLA5ZOC3CIcAh6JOLrF9FAfRcoFCBhaRj2zwt89zFIKrGJJbcA9TnJBKTDDwkkBiQLxzEYBM5DC8QH8XRCsky1bwdrh8pqQNlymc8aZBdD9W0rqiZyex6FYuJfp5phiRiLIxcI/6tjs6tF3HdvVsxmKl7oECOCbRl2UjpsMNMA3yZh4dHR2t20rnqKmVR02onaN1GukcNTVyBSLeRkY4e3pXiNwhMq6bc2kpKkoZcy+ADFFWM3+rp2GYzzM+zQ5206ximmUcR0NcTLkumP7uh2M59WzOiHZEZgp7sCQMUXDJ4RRs4Nj9MefpoxIczttrgBmMOdtwMHFE2YwgWie3q8Y95ausqinCB/agasTbDnhCERlzPXWS6ak8NYaU3kfEL+f4N+W0PUCTmwvI5mPQ6aQp6AumDIezkwDihZJOWUTgDJ0EkFIlGXoeovRjxHvjEkH/M8EMfQo9lFXDX3mXvseWBTj+jxK9Nv/arIKKQt6tnJap6r1Bj66auzXTtmAXKXyy8f5RiCZt9LMfEt6b4aCvJb+rGKIs7+fq4c4A1g47C1Jsh8Oh5DfZeyeIJ/buIOkF+KanVOt5iLCeLGV7hCmVfkLLFnVu0VLU8QKMQrYKkSyVI5I/VyBK6+SI4EokMEfAAnqaKWf7nOP2v7VSu/taf/r+j6FFLFbj3mSOAq6B2yzeeCu4Yv/XHwxM+8+oPxrs9n8v8e2BkyheEjybM9B3+n3wy8d7SND34Cz0bGvP2gMfsIdCroYkoY+I0DeOY+jNUZbzPUhNRqBvO+BvvEAnzer8/a21B5ZRAhZwCcKI8UUWsDmmYIoDoQihmHG1yIsWcYD5EgDuMZsLNCkQ29oDv6YgohsGcQgg8KJ4yfUspRyATBDMddRxr3d/f29DQShXTXqBLER7H85OTs+vTrt92xHFfw4DRCkg6PcEE+SDmyWAcRxgT+h9AbwHEQFwRpDU/nAI7gnm6/n3gEZTxnvL2gM+3xfjm4RpHZVRhqlWIAoBDEHn+AqcXXXAD8dXZ1ffW3vg89n1j59+vgafjy8vj8+vz06vwKdLcPLp/N3Z9dmn8yvw6V/g+PxX8NPZ+bvvAcJsjghAX2LC6Y8IwLwLkc/76wohjYBpJAmiMfLwFHsggOEsgTMEZtEdIiEOZyBGZIEpH0gKYOhbeyDAC8zkAl9ulG1ZDw+919Lyl/deAG9QQMHr3uOj9fDQBT6ackW1I1REmdkB3cfH1Ab48ADsX6TRKTc/gD/A70nEEACPj1ZunzBLnvlaueg+5Mv5bXKDOCorVctNU8YcEcyEKsThXcrtvs11J+whDie1AOj5gqrHR0vYN2WWtJRnGbylKPRFw2SvLCDz5h+aOkMp8Sw9UqLJOiEIMskXHFc2fbwoZCQKAkSEUsI7AnpelISM83tCkVVqQFruWBbjJMom8DJ4WpBl2nns1IojCnPd4eGBw4RJwCpanTUioKiqQif9o2O2Vvn7W4vXP/1Xt/4ru4NephN3pc1tbfPwivX/YOQO9PW/33eHO//Pi3y79X+3/m+w/qvSPrXI25m4sFMzBBfPqpsRxjHt3bnWLQ79MXiXm8CsBWLQhwwKm75YDJW9vtQdpF2BC62PUeIHahFOTIpQ1zZs8AfAoY9CBoacFt4H0pkhRpmOgWsBQBmBDM00y8Ul8sSSybM124eydmemtXY0aYu+QtgIiPUtk78pFqU/+BdoCDfohkOQrqZZV4g9fxTyuYWIgqFbHofsSy1KDw9yvmZMIJLtzE8EHh8fHkBMcMimoPP/aY+TIdL4+iz+qOceDgn8USz3WQ5f78cra17DmVrZHdiDTtpqpQEXig9LASlbUTi4gFYThXdj5WfWSRefrq7fX55eTX6+Or1U8gEQblUNQYnmhCKSq28athL4i+Orq8+fLt+tiyKzZLZE8+6HdRH4N9Wg44gwOtagdQt2M0x3KbYoSBacucNyzQVPlTbW3D6V0fB70OMzRqthCpNuRYncaqvkcTVS4+1Sg1WjaNFkw93w8ABY9CtcBJUzUKKRs6E1XsXwWlTTrLHPgDQz7RZ1CmPv5ujkjDP9xkWFkke5GueoAWd50yLYOZO51bsau6hPEFcVWSYujoN7uMwkouRXhVXTnRBXefKNTeHQzvdByjwxZa3JpmgRs+U7TMbg4VFDI/ZHawDK6WC/CKqlX0KbEB5POjf3oyWW0DwbirQtZLXA3VGpU4al2+1aRvyR1AnS/fhLKwSKnMr6sMO8uJsvPGkjYlNiMUhmiJUEWcoD6PeGmbyMEehkUQMd0Pmg+EI6ukyR8JvGIyvVMAliErHIi4IxuD65sDJFpwmoIFHUN3b3pZK6H0fWMX07nSZUJQAdy2iCroZtpnMNMx5M2wVDv3Gu/k2dzM1z4e8ctOTli6rZZrJ9PaNnk3f73F74GWnqies28kFRXg4FQTRKiJcJPb5/QrRYrVPvZjNvUfy1JW+pzlKQ24Ky+dXpdtapqqZIKZd6X3VxWi7W2JYaCnPeVX5of2Z/fZP9fxv7T+qFfHJUYLP9x+07zsiw/zgDd+f/eZFvZ//Z2X+2bP8xY1K2YAeqjFWpWxIrCv+JbENtqHs2K9ETu2Zte1FNbJGylXsGy1GZ70wLUpmsOltSNSzDpiTjFrdlVSpZSVrOriL8RodXYWRp09A0vEmHxbvd2GVWIjh0Dh2jmhgtsL1mrWsZMiOXKk1DKmMYhQr9rEhbZQD88dPVdUvjWandarBbW4Pgp8uN0Ykxb4duDfNmLbqf25s7W9sha5G9q7FLbmpZre/MVpbWf3388PH4/Pj96eXk+sPV5PT8+IcPp0/GrcyXSqwbTrsaoq9OL385vZycnD6dA4voybW6K8X80+mvmyH+CS3Xwnvy4ez0/HqjFhdhnE/BvEGL81DQ9fAeb9ZauHZLNxFo2fpVNwnaWb1LYF/Y1F9u1gua/EvId6b/BtP/BoK15DWo0p8NfYSKTtA1H5l23lS14PwnWN1ffPtVZYnn2/puhQavGeUVBVQ1yivJ9Ub58pBtwzhfKUnWNNIbvUBXd0N2NEHvhifp/3/FDqv1alST3GSBLtV4qpdjNaA1vB2bWBEUv8c3NT3vvj/BV2f/F2ek5b8bXwewwv4/5H/r5z8OnJG7s/+/xLez/+/s/1u2/0uhoRj9pXp5Io5Nf4Txaue3gNCV56wbNE15j0Nr5TLDoZyRFqJtDP4QoFO77rjQKdSD8930ZHxxeYT8lBPxRwNHtX4qx+yPBq6TOoRLZzYeH58VI6i4bSCDnOXUAT+sBT5yhA+biFPsr27R8nvw6g4GYPyPGnZQz9IXRzl4TdEB9Ts6TSGSsDhjZIMm/tkT0ZMgjG4if/lWJN1H5BaRSUwiD1GKKAD9t0oFREhEJkE047rbjPbEbzuIZm9X5HMsDHtIllOK9Xx016PMjxIGxGUAtSUQIWm6SlGM/aynBUbZ1Bj7SiEJ8Q6FjIKHfGjSpnpRGCJPTlbgOv2hrPhoVOcDrFTOpo78FniBhEpM3+Yl0mkxEZpyJho5ml7kMcS6lBEEFwqVaRMm8j4EABZcYoP9VwQtIoYm0PcJ6ILspxi6f79ieIEmQeTB4D+g8yoNJumAzivBkJxdO2DfsN2r3/4ryiBLKHjFeWBys2SITijnohQEQVNE+C6kEUpamBM1gTMUFiR84e25h8RHPv9rFTVJLPtlkpGVJ4j2N1dOmz/hnaLUJIjGUUiRSG+CUAxe/oeM3lE4WiZIluYjZAwgRaEvluj0i8IC6B7z4kkYxQmda3l5gVuEYhjgO0mpmBGjN0V9sZRQLyKITnA4mSPIf5fB7M2+4lhHHSRwEkPvFs7QJIZsDjqvYoKm+EsvSGDvn3aQwLdvO28V9sZskk6Qm+WEVxet2ufFeSaGAf6KeL19A3vW7VIkZtd8PBiGF771Am7/wHZsx3bHXCC/FZca/JZQxld5HN7BAPuADztfpyEFEMQB9BCYR4GveUYBuEn3g2VisxyFVKBPb41oIa1bE+1+U6IzGXTHp3nvtZDvJeGVtuuhSsRwfhSLpyFldLHSNGPEtMusCKCQJYUYSf8myEP4DvmroVEkdCk5iTv6/G+WHlIQ5RUKIjqglJzRo2WlS4FA3TH5WpEEQhAwL+4qwiDvy5JAEFyjs1IgIhvBkds/fKtlCAgaHVwKuLSqVJY7qMyNIaUKrw40VI8pl5SMmmvElmTq1Za0zHIEiXJk1a3TjtLCWdzh9sJNaqh/tgiT9r21dlCJqQc/VxxJun8xg0dEcl28iFLnWYNEvGixgKFvBj70bnDYu4F0bqR3PSPhD0Ps8H19F4MO3eNCt2dujWTqIvLxFPPEcoFZR46LWH/VXL4SG8iQN49AJ736Sqze0bS0GwM0EcLo/3WM2lGMQoIoW4LuDOz7EC3ETVvTt6oobXPUyNg0VZZw1RJtQ074HOnFJPoNeYz2ON/3hAxLjWmiL4sdjNE8c+dLzZHKTijVQFifFGVYW9JS6zGUNL2wj1QifUnHqMS484aucxCq28jZXmYU0udVhRnoCW7LZ1rZ65yTugtOkTKqC05JrnespZy9DW+aBKXeWvo0vyPfT5gNdKsb6H6DBqp3r27DT6gQ2OQclMWe6hGsqb2GG3BN7a7C81fn/4lpvLVroFfc/+EeuK7h/3kz7Pd3/p+X+Hb+n53/ZwP/T8vrnoyTH7HI7d25N4jBbB2/MGtrK3rt+Ucat1/BM8Wg8o4tdYUn+A4HaFZcN86F8AccJuk14yThuC+T8Jgeh0uem8RxgBYoZDB4T6IkpjUFCf9TXD1anT+lonpNrqJddcH+633FO/LE+V8n/0n0hIuear5V8n80Mu9/fPPmYHf+70W+nfzfyf/nl//mAkBuoGfDhM0jgr8KBPbtIbVxVNhsL6MAtVoBuKTa3hJAskclugDGOBXlcvPw7325bu3/x0p3wen59yLbp2n7RUmMaFr2DpGbrJwsm1BkwOEU8DL/riHtP6orfGvzv0n+3+DQx+HsmeO/HPdgNDTjv3bvv7zQt5P/O/n/4vp/S/H/g5Q/rVYB3WS5xfUgCtAlmooLU9L1oIF+CwBl9Vq53UhuhH1eLjeaNTNrR2EMrQOSlaAx9CquLRbJRhyVOv/r5H/an+lFwJstAavk/7Bfkv+jwe7+1xf5dvJ/J/9fXP6bvptM2n1LQf8cyvVf4KuT/+l7XenhIuPnmheBrzj/Mej3zfu/BwdvnJ38f4lvJ/938n/L5z+0BwE3OgdiSJ2VB0L08quUZ/WxDOEcbvGwRlrUfG1EhDJUvziStYjSYLJIAoY9RJgtm1KcPKFsguN/vBaleIEJp/sfImTHFhVitBCZt2ip5t2iJc8S8YZeRHxqQD75dP6vs/cgBSSyRDCA7YmJIQ4DnJ1fA6e2NEF3iFAkY0LzBb25TkKCCUELGNvi30k+t9pWi4m4TBOJwP/J3Ccraoom5eHxcvgnZVqb66fVRHgHuLq+PDt/Lw6Oi3/GOL4bgeFwMKY0aAEsC33VoA2HAzAavXG6owPHES9XOt0B/68ZIKWBfXIs+YCLTfF4SwrSgzl/tKovwu7Sur0oZjIsbDX+7PW6zWFw6kvNoCHu0RALjUJEKGwqK2iI/6qCIu2GbArnz3cWB66aJa4aUUlDXES/ZG92crL5X2poqXyvc0Lkg528hPKCZ8V1phsEXJc6fzuj9KRhesI4rRngbYzNswV6V3RWu+4qgqNa99fTw8MrqVxr8q07snWj2zDCoFVQ+nOFnxvqkxmGrj2kXBeOXgHjWcPSayZ23s8bRq2LRUZruKiapmivs6YM0iIM3LirsKJEcYkKWCMKHDHPoFVXzSpjrSs03doA8AZwqyipUkI3pmclUJ2q8lhW0ClW75VkmcXKiArNZFULEWFdrpVsSLmusj8ds9K/DSCfSl+xbdgWeTrE2gh6XTC1ODCwxeh9HfdLnhvQMe/ODzzl/MAKidR4kKDOjtCIQRcu68BXa9aRXzGrVt3sVgFivVMRf6XdUI1O0XQGYzgc6CcUqi9BK1IbzxWUJZV+tqD1UQJD7Kw6UqAXf+rRghVQ1jhi8DTmWHMLpp1J+NYm8f9Tn+7/secoWOBZGBG0RRwr/P+O88b0/w+d0cHO//MS357YnKGQWfYMM+u1cAG+tl73sr/4v/+0rNfoyzZZYvf9ST59/ot7lmk30+S66AtcxBsfBFgx/4fDvnn+62Aw2sV/vsiXXvCr6V5KOJmeId6/P6+oITIUJdQe2W53ivzAW3TvHNu1neLZesfKjGPi2a1YsTdZpc0JsDL1brHMqt2ipaW8z5Yd0cGU4YjrL6nfJ0svRaVUlEnVJhmIp+lOIiU7SNxV3x/sAvNGV8va0ytzeHuZoXCKfL63RD7EPRNFWugazsZAWuZEYmFiHojf+s6Yp2gbVp5QbCVFtlBKs4OxOQyp9A4cxzkQaaZG6R71bXd0aDu2K+koPArpU3F7hVtiL/cruJzxbRGFE5Fsf7UHFEeCCro/5jP+yIBxdHR01BrEYDxwnKMj3u9iiGr7OxtAtZ8lkxIZK/ls3a2evJZdLmGrB5Zl+uGTh4L32RjknSWvEVSu0crv+8uAHdmu+iLLnnrrH+/RgZapXPzHMw8LTPnNgiuQNeE6aMLVT1soOGutJrpOYxtdp6GNrrNuG12nqZGu09BI1+lz/s0Ei8bCirTROHdgP5FBEyp22sWTOCI1exi6nOPflNNyw594M3EPaA9f5qnqa4h5YvGE5BhcIuh/JpihT6Enpx/FX/nsfo95f5iytXZqV95Nr/ZWuv5sX362mq/qcz7GgKpP7+TvxhbJ7yr6Xn0+pz73onY808vd8wvi90Dx+krDm0nZC5Be2gf5yykt6vDVmtcpHj1pqiRL5YjyB0ta1MkRwZVIYI6geJVhDPYZSdD+t1bIXvhr1P+3dAB4hf5/0H/TN8//Hgx38Z8v8mkWuuJKYis36cmcMI2Slhq6DH/IlHien/u7FRdzeoNLbDiUFR+yUvhseh6xC4LkNYe5gVS6Nqq9GS09GX1gvr5rFZBLmwPR9sYdA28Dyl/ayhog9hFGHLml7ityBTW/71KAmkOaXy2XWW/VcBtJpiiQJReBB2l0gJoulrw86xrOKoBVeN3aXNI1qggD0iFX+NRWe9RWgzUdZivcZXUACwu9MMIrzHcifRBnF/mlQ1pwjxG1AwylXhSpumFJ1/FFsap7isoqvRrrZVyuXXGPtnaTtjp2olILL6Lx0wLlXWwF02plmphXK7hj4j8LE+veL9PBVcWUVZGIGwQbXraNNazmUXXHVMGeWXYTZ2ZlNmPKdYZkx8ByC8p7S9z8njsKsy2L1EPS9zPTXL4N5en+TZaSb0J5cvojyzM2pGK9VpNyGNoWVQCqeMJf3bPyMsXvHI7Yt4r6+Gsd/1qgcktbwbhmsSYGNsvupOvLSddm+WnaBXiZKmc+MCwEgv31J4BzGgqRZlQtyDCe882rvhkO+qVq7270SuKdXPWx7GzbXKr5cz6JjQd9W9W+0Cd7+dHcRii5DUMMU1Bq6uFwOMgLq+YNMUPVx2bzKm0MHwZIYZMoIMo3VdsDvEXLHKBqGeEQi9+tIBYmEwNiTqL67Gt7gBqJUCEPtu8+aWlRh+5U3UZpbxYrEc4J6pQn6LfeKe++3bf7dt9/1/e/AQAA//9sFvHkALYAAA==` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} diff --git a/server/infrastructure/gorm/mock/chart_openfl_010.go b/server/infrastructure/gorm/mock/chart_openfl_010.go new file mode 100644 index 00000000..c82f60e5 --- /dev/null +++ b/server/infrastructure/gorm/mock/chart_openfl_010.go @@ -0,0 +1,78 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import "encoding/base64" + +var ( + FedLCMOpenFLDirector010ChartArchiveContent = getFedLCMOpenFLDirector010ChartArchiveContent() + + FedLCMOpenFLEnvoy010ChartArchiveContent = getFedLCMOpenFLEnvoy010ChartArchiveContent() +) + +func getFedLCMOpenFLDirector010ChartArchiveContent() []byte { + base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOw9/W/buJL9WX/FwO4hd71Klh3n4/lQHLLp9m3wumk2yXaxWBQFLY1tvtCilqSS+Nz87weK+pYduUnqdN/TFGhsfgzJITkfnKHJQwwmzPapQE9x0TueEaGcBZmzF08Gruu6+8Nh/Nd13epfd3d48KI/HOzvDfeH7uDghdsf7rq7L8B9ui6sh0gqIl64j26rOri/CJCQfkQhKQ9GcN23SBhmXytLw752nb7jWj5KT9BQxWWO4Cdkc/D0qoEJF0klSCu9hjGR6AMPgE8m1KOEwYcQg3fvweOBIjRAAXROpmgFZI61Ri3JI+GhHFk2zJQK5ajXm1I1i8aOx+e9d+ijIAr9o5PeP6Ixvju6/NGZUmVdZ2MynX5uMn+3UN3/14RFKJ+WATTs//5gv7b/94bt/t8KxHtvZAEInFKpxGIEk3RTEWoBhBFjZ5xRbzGCk8kpV2cCJQbKArNvzyLGLtATqOTI6gLYEG9kywq5f4FeJKhaJPUtAAzImKE/gglhEi2LBlOBUuqsgCscc36lPwN0gQQBV0SzGWmSZlyq5GPWTFbLMQvZwVsyDxlq7pCUBAiJmo2gF39XTI7gj0+WNed+xDDGly5+g9tQRFOBeXPboM0zLsk0Yyo6UcbNXcxIiCPo/LHT3/nUMQ0RMUW1IgODa774CQlTs+MZeldnKCj3R7DvJgMPuI8XyApd6oLiTM9JTowukMmEBlQtzHe10O2cch/PuFAFTPprWoVx4v9AGAk8FCdnI6tG9a8bexdCIuUNF/5o211/7m3zLwNV/q9wHjKiUPY+z5CFKKSjwkeKggb+PxjsuhX+v7+/f9Dy/21AF455uBB0OlMwcAcD+PjzDRH4Gk4Cz7G6VhfeUw8DrcNFgY8C1AzhKCTeDNOc15CojDBwXPhPXaCTZHX+63+sLix4BHOy0LwGIomgZlTChDIEvPUwVEAD8Pg8ZFRvb7ihahY3kyBxrC78nqDgY60zAgGPhwvgk2I5ICrusNYTR73ezc2NQ+KOOlxMe8wUkr33J8c/nl78aA8cNy7+a8BQShD4Z0QF+jBeAAlDRj0tqYCRG+ACyFQg+qC47uuNoIoG09cg+URpalld8LXwpONIlQiV9ozKUgEeAAmgc3QBJxcd+OHo4uTitdWF304uf/rw6yX8dnR+fnR6efLjBXw4h+MPp29PLk8+nF7Ah3dwdPo7/OPk9O1rQKpmKABvQy1BdSepJiH6ml4XiKUOaNVcf5chenRCPWAkmEZkijDl1ygCGkwhRDGnUk+kBBL4VhcYndNEBNcH5VjWctl7ZQyAjHqMjJFJeNW7u7OWSxt8nNAAoVPhM44p1wH77i7R/JdLcD4a7VMnwBf4M+IKAe7uLH4ToBjBVTTGCVFoeSySSqdUzYUZCqpiGabRnSNDItG5QHFNPdSYhEkq55/q9u7urNiMMVnGDk4z9Egw8OPemlHPifJm7zccbKHwpiOutWgdCyTKzGpcOln82owSnDEUIJNhEs/jUaD0ao0kWrXuJeWOTDE9RtMrXYZOsj7VFDgn0d7iwlosL5caJ4mYKg8k7T+TuKpsJ/nQqQ608Pm5+eK/C6yX/1mSx4MJnc5J+FCrsMn+OzjYK8v/Qd8dDFv5vw1o5X8r/x8s/yuHhz5RpGhPfzacI2YbI/hi7GVUmnaZHW/s58/S2MnLZSpHEvPcyWRowdBOJEpqY29QuWCMZ5VjO/zzLDbEP3vaEv8cJqb4PZhWW+8ZUkalwuDzjEs1AteJ/8Xma5wO7355exrPVGhd0cAfwXFMoZ9JaM1RkZSARjkyJDIMepQR1VougQYei/x7tCoHvgANfAwUDCHunNE4srNcMzOb8X8fQ8YXcwwe6hdo4P97u/0a/x/stud/W4GW/7f8/0n4PwlD2bvuJ3ztbcYztsvYLD1Ac5wdT6EcQd/SUqd4Jlkwx1Ix9FWdKZlzhR7tmx5JJYjCael08xy92HyzAFIOm/SlQJ1YgJS69XgqHUIim1LCaMhcb4WGbL0F5yTw8yST3BvToDcmclZJt71KwpfSd4D5lU8F2CHccHElQ+Jhj/GprJTy/Dy/kjW5zYYNmksrsO2KBLNDomYrFQ6wbcG5sj0UypTSn3o67bNHHC/GFgp6TRTaV7golNGpzhUudIFozKhXRZIR3EvOqw0kp+fLpWFhqQ4RJzupfwXu7pZLCAUN1AQ6/yF7erriNG35xh/WKh8xJviSG9Klg3ptTo8aq1+SaRFDLOtVJ1NgsnGcFfw+BaRmMLlTCEo1K5sxzwi5ULK6srJlGB/0w57r7vUrS8AgNNqTHfISuWO8givucTaCy+OzQt41Z9Ecf+ZRUG92rlPPjE/IUK6Xr9BVS2llnyoLsVJGIPE/BGwxAiWi6rqW0dg039hYQ2fjxdjQOyzRTNelwfQtFRm+JHe5tMvLtrZ8is6dfNrLLp/lEhT/XW+/VTzItGLW+cbNFvxHebWSU+kbNJp6qPI6uc/q8c2ZTVR1n+YV6o7Vr26zfsYXb+NU+Kw+BHTy+mYDVeRDYquUN1TTdrg/325YsDKmQLlFk3Zar/fcyvRfEDaw/5KV8vCYkCb/3+6wX7H/3D239f9tBVr7r7X/nur8z5h+ibPtmey+gqJZFS2JDAlryuYqJdKcGBrFtK5+Jp4y/PMetWURInTSqJYOdN4XIlg6Zf3JNHOf6p6WWivx08GS6bQ2zsGG40zLlq3l2nylNu19/Y1HH3et4lSslSwH9pg6lWAf6NzXVA1BxypS57kZ7HcO6+V/Epr3BKGgjfJ/rxr/uX/Q32vl/zaglf+t/H8S+R+gSg4XnKtD6VCeHwafGE5S0gjK0btrVIQs+yEqQkXwpOwsCxguxBcbqVMKOC4Y3JsgqDedyObsPDoNNtZ5ggRTXI84DnQ2fbLBuBK1AExDazSF4jWeinWiZoW4aBPtXAy36XXAiQ8vC2d1+vtlLMbPBE7obZYxJt4Vlk+BEwNwldlfmMAcwixut1A8mo9RjODw8PCwJJ8b50mxhBaKbTYvusK6+XhudvvdwXr5n9Lzm/t/hwd7Vfu/P2jvf2wHWvnfyv/n9f8+SsjX5NAj/b8bdeY5/L8PpdIz+n//GYULhUIPCWw7HYDuLfQiKXqMe4T1GB33woWa8WDXOezpXWKHxLsiU5SJg8pWkeKCEibBtglj/Cb27IJt0/BNGmNl26dJA0dh6Ch+hcGbnZ1Kcnpd6c2K84RMfUgLQd0n+g18u1mzD/Ptlqtvwbe7QuHbxLebqH0ljTDGlyySrfh217tLs8XZ7C7tVaMV/ECmtDw21xLeUSFVk6sxm7ntulVzLXmLbtXcZPp3d6sKjANZ0gVzxG7IQq53ud6/Opt8o6V6a/S/DfT/b+7/Gwx2a/6/fqv/bwVa/b/V/5/H//dt9P4V/r+KyhJWFZImt9gKFWW9/y+Xr0/h/yspCff4/1Z77AoDX+uxK/f3Po9dVvKhHrv1CFqP3bPBevkfygdf+KtA4/2//m7V/+cO3Vb+bwNa+d/K/0fc/9/wwvjdXUlXCOPc3nV/jIqkesNZtXajBhHK8FHKQ/X+fcFdFwp6TRlO858q0kL2PQ2iW9MHETEcwXkUHMmjYKFzozBkOMdAEfZ3waNQriko9MdfJYo1+RMZV1+TWzASbdh5tfNY39Z6/i84e4TNV4Qm/r+/X+P/g/b3/7YDLf9v+f/W+b8YE88hkZpxQf8vxl+LGTnnrNmE1CzqqSVAFqhhAwlpwsmNyfXHjhFcO5+s5Cwv+XHKPNuXCQHikhRlUvYaxTgtZ8pGEit4Tsk8xvVHvVefiiElT7r/7+f/Yxr4NJg+Ugw06v97wwr/P9jbbeP/twIt/2/5//fK/38w3KdRDJR9Lk8uEDjDc5zoJlOBcM8ILICCALvP0ojG/0RPJaKmdHSaDiT3T66on2bGrsj6z5nFyZVYxFX7fz3/T6ia/JjYY0RAE/8fDvar/p/BXvv7T1uBlv+3/H/r/L/qK0oZ3vMy+m+lYn/XUOX/zgzZnE4DLvDJ2mjg/65b0/+He+35z3agG4f9YaCs+N2EV7EC8Mp61Us/6f//17Je4e3TLYgWvhtY/f6DnaqBdvp7+o85AWi2/2v3/912/28HVr+6UjAsqlnxbySfrqwVZ32svLySvyvRKTws0bHyFwU6pmTHWvvURHf1UxNZhMd8kbZyhYuNHp4ovP5gF28223nQhG41f52i2/w+Rbf4QkU369wGT1R0S09UdM1NpxTL2oi+rHLpYYyk6XvatLr5gxfdNW8+dFc8+dCtv3ZxcDjc+dTROSufu+je+9pFtxrl2q0EoHaL4aHd2kMR3ULwjP5SCisJR5bVzafsK8eZvWsBREx5MBi9NH+p//L6Tf9vL+dv+u5g6L5Wb/ru6/DN4cvL+W97Lvn54B0uBuxU/HpwFX6Y/fJSDi9+P2CHv5yo2+n53+jNx6P/fnvpDaKbvwfkZtYP+/4Pw8F4/P6n4eG3Jsea/d/A/5/EAdjA/3d36/Efu8P9lv9vA3JrKDWDrBVPAun87JJD+Y5CxtGNdRVWrhYUbhMU6hW5eyePOjPR3KsDuDcM3h7k2BJbzsox1ySDzr9fXOgxpFZsPoBYiFRsxkI7idzQqQURkuenfCkxSEuCJS9Vu6C8/opy7Q5SLXQ9R1u4XZy9qfRAVNnV3Prl3CY0lQvAKQ3XvsqUN5r9JGSCNr+Zk9yhSS88lWSVzi6kJEGHVbmlSxVSslLrpJhZHavyiqt93+3USLfq1scGdz42mJP6vY7mWx3NaKs3NxrubaxDmMehxvGmBSLlkbIFauSBsdUA2GrAqS6zKjC1zArWb7Y0vWFR5YpBzNwqV8Xa6X3O6X1uUdpCCy208JeC/w8AAP//5PKPOwB6AAA=` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} + +func getFedLCMOpenFLEnvoy010ChartArchiveContent() []byte { + base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOxccXPiNhbfv/0p3kBv9m5nMYYE0qNzc0OTbJfpLsmFdDs7bScV9gN0EZIryUkoy3e/kWWDbSDkkpTd6/n9kYD09PQky7/3nqSHCJGPWA35jZjVjydEandGpuzFc5LneV778DD+73le8b/XbB++aBw2263D9qHXPHrhNQ7azfYL8J5Viy0UKU3kC+/JfRUH9z9CJKQfUCoqeAduGg4Jw+XX7NKo3Xhuw/WcAJUvaahjhi68RTYF36waGAmZtIC4hcPJFPNCHCUi6aPqODWYaB2qTr0+pnoSDV1fTOtvMEBJNAbdXv37aIhvupen7phq52apoFXic8/Zn4ly7/8NYRGqZweAXe9/68grvP+HXvuwfP/3QfYtte94w3HolIyx4wBIHFOl5awDo/S1JNQBCCPGzgWj/qwDvVFf6HOJCrl2AOK25xFjA/QlatVxqgA1iHtwnFAEA/QjSfUsae8AICdDhkEHRoQpdJypCCJmAAKsSuZDIjhWhPnTml2xq4pLMl4igykMqERfC/nmt4B3lt9yVb2wA42/N91G+2u34TZydedC6g60PK9ly8l4vCpqxkWxaseCj+hYWQ2rwEWAA2SxiLRMC2Zmjgq+ZCOjEeVUzzqf+7mnlHv/NU5DRjSq+tUEWYhSuTp8OhTseP+bzcZR4f1vN5uN8v3fB1XhWIQzSccTDU2v2YQP72+JxNfQ477rVJ0qvKM+coUBRDxACXqC0A2JP8G05jUkLgM0XQ/+ahgqSVXlb984VZiJCKZkBlxoiBSCnlAFI8oQ8M7HUAPl4ItpyCjhPsIt1ZO4m0SI61ThYyJCDDWhHAj4IpyBGGX5gOhYYeNadOr129tbl8SKukKO68wyqfq73vFpf3Baa7pezP4DZ6gUSPwtohIDGM6AhCGjvoEmYOQWhAQylogBaGF0vZVUUz5+DUqMtJktpwqBQUs6jHRuolLNqMoxCA6EQ6U7gN6gAt92B73Ba6cKP/Yu3579cAk/di8uuv3L3ukAzi7g+Kx/0rvsnfUHcPYGuv2P8H2vf/IakOoJSsC7UBr9hQRqphADM18DxJwCxjsz31WIPh1RHxjh44iMEcbiBiWnfAwhyilV5kEqIDxwqsDolGoLYOuDch1nPq+/sj7gcvYYGSJT8Kq+WDjzeQ0CHFGOUMnijGuZKlBbLBI3cT4H94P1PkwBfILfIqERYLFwxC1H2YHraIgjotHxWaS0Kcn5lhOUVMd2wsi6QIZEoTtAeUN9NGKkLcrX901ni4UT+7C2ygZBaYUZA/IgVtWOd0q0P3n3kGFmOB861rXunGOJRNuHGXMna94XXEvBGEpQyRiJ74uIa7NII4XOmm4JX9eymQFarQwPHS11WjPUbmKlY2ZjxuZzI5NETOcHkurPFG7irSQfKsWBZj5/bjj8v6Mt9t9+92MfZ0rCp4UEO+x/4/CgaP+P2kel/d8Llfa/tP+Ptv+FzaOAaLIM3a4seMTI0YFPznwOWnwkU7a0GUmw51pjmQ2q4BNQHiDXcGhsxTXlQQds3XsSOlPUJO3LuhIdI55yn0XBNl/DLQg1xskyJuGvA5ANhq36n/v1/MPpXvwPMGRiNkX+tE3hHfjfOmi18vjf9BqNEv/3QiX+l/j/LPhPwlDVbxoJWJ8skWMjWq+D7yPwO4PWjhmX3baMn5zqQMMBULkNuUw4ZgvWDUAKgEmDjOaGWK7temuAVA1DJkIjlKPMtKiZdT4lPFgV2eL6kPL6kKhJobzmFwoqIQ2BcqUJY1CTEM70RPB6snrNhCtX3+lv4Pzj5duz/nn38u0/vpqvviw6v4a3wa912w5Gd1Z7MCiooVZTEyKDWhzO/Vz56rT/4ezjVb/7/vTnCtRq6R5pbSKUNvUnvYvT48uzi6s3/zrp51lCIfMs52cXlzGLFELXfJS6FhI9AfOpbsqufOL6sRKhpDdEY+0aZxkeU+pe48wwRENG/aIQu2CsiKwRtyxrXkklM7XIb4qPxC6v1QzkqgHis5L1YD7xLIpichO1U1LeM8puaO+Ub2Z5s/zKzg7OzSNbLCobe+h+992jhCf75wW5yZb+fG7RPm0aF7vp2QMsFvM5hJJyPYLKX1TdoEBchjywHzb3GYuBT6udh9zRQQUWi879bS/JONs89ol0JTf3y9OO9DQkI9EOY3VUkn9qWdzKTKRg0RTfi4hrVVyKU1N6TvSkA3U7hvqtkNcqJD7W19Z14QmtO7UFBokkOONs1gEtIyxUqmhoO76/mx06Jih1j2aWI1FQPVzDHR3HuHDfhKDUmWrTkPLxCZUbhCV8Bvu6jBKFOWA3xUby2sN7xHtNwwfAQS9cNZIY43e6FLvslszSSbQLq2CEklAqr+u9K+WeygcJ3PqAH8BUu++Zqfi0Md+xLesXG31uZ/sLpC3xX6ieuOeXpV37f0eN4vl/66DdLOO/fVAZ/5Xx3xPO/x54crRY5GLFMK6t3zSGqEkaMp4XW++MHEMVPj5u3OC4p9GbCTQow/HqYoqxKO8oj+6sAjJi2IGLiHdVlxsnTkVhyOLoi7DvpIhCtYVRmo8/KJRb6kcqbr6lNmPKa/Dy1cvlydnjD8624L8UDJ/NAOzC/3Z7Df9bzVaJ//ugEv9L/N87/ssh8V0S6YmQ9PdYvnv9tXKpWG0gXgiGOw2AQalntQAyvX1YAxLSBMltSPHTS2u1Xv7iJBFXcpl5VR2oZPQxJ0WV8N6gHKZ8ljdSWJDTj0NHgJ/WtfolA/PPHsLcg/9DygPKx083Azv9/1ajeP7vtcvzn71Qif8l/n+p+P+tBaCdZiB/se15DYJgeIEj019qEO5R3wHIWK/7Io1o+G/0dWJqbJtBfhSrrbEtRwzxZmNI/A03GuPi7C277YZjC/4nU5pcKXyiCdh5/6t5UMT/1lGZ/7UXKvG/xP+94/8S5QuY9xmB/g90sb9oyuG/O0E2pWMuJD5nHzvw3/NaxfzfQ+/AK/F/H1SNb6Qg106cavsq9gFeOa/q6Sfz95+O8wrvnnVNlPRl0Ib831rqBtbwjkzDp28E7/L/ms120f87KPd/90P5/N9MTJHL/m/YBKn+ekp/XP6hkKC/Sh6uZLKHK87W5OH11OHVof90loq7xtmD0ogzWcS15J6NU03yiatb0omrG7KJq9uSiV3byE3fD19Ms8yF9GI3W5dNL66uZRdXi8nFVeP+hESSafoFwI8CchWg8YRUB376xZaL+EcZCLsKWTSm/Mo41IIj16oD84XliS/3XaW/4SDkUuTy3iNMOVX6qsjovjfFA1N6sixMGxfUA5CEX1/dCskCRX/HDjReQzy0fI50FfIJ0mY2vrTs6D8/3Yf/z3UAuAv/W0fF+L915JX7v3uhVUCURkKbfgPC1C9vZebvVS7R3QZYYeFGZOYSZKZdFvyThiYAs3dBi8bA1q+biExG0SrOa66kJeGcs5K8ZjxM/f0WxYwhjWJXA4jtTCFs3PLjFave7UXrZPdudQE2ua2aLY+N0LLqkozT2rw5MhybLhFm7VCWZ3VjMG+NsjzJXd38T18YhtU93g2/gZF5FJvzuNqwzAdOpyNrDZZ7mjkLsfEB5yQlU58XnLEqaWXO0DxSbGqclrOTGqv/XuDTrmyUVFJJJT0L/ScAAP//dkPwfgBQAAA=` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} + +const ( + DefaultEnvoyConfig = ` +shard_descriptor: + template: dummy_shard_descriptor.FedLCMDummyShardDescriptor` +) + +var DefaultEnvoyPythonConfig = map[string]string{"dummy_shard_descriptor.py": ` +from typing import Iterable +from typing import List +import logging + +from openfl.interface.interactive_api.shard_descriptor import DummyShardDescriptor + +logger = logging.getLogger(__name__) + +class FedLCMDummyShardDescriptor(DummyShardDescriptor): + """Dummy shard descriptor class.""" + + def __init__(self) -> None: + """Initialize DummyShardDescriptor.""" + super().__init__(['1'], ['1'], 128) + + @property + def sample_shape(self) -> List[str]: + """Return the sample shape info.""" + return ['1'] + + @property + def target_shape(self) -> List[str]: + """Return the target shape info.""" + return ['1'] + + @property + def dataset_description(self) -> str: + logger.info( + f'Sample shape: {self.sample_shape}, ' + f'target shape: {self.target_shape}' + ) + """Return the dataset description.""" + return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data ' \ + 'loader to load your local data for each Envoy.' +`} diff --git a/server/infrastructure/gorm/participant_fate_repo.go b/server/infrastructure/gorm/participant_fate_repo.go new file mode 100644 index 00000000..bed39129 --- /dev/null +++ b/server/infrastructure/gorm/participant_fate_repo.go @@ -0,0 +1,129 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// ParticipantFATERepo implements the repo.ParticipantFATERepository interface +type ParticipantFATERepo struct{} + +var _ repo.ParticipantFATERepository = (*ParticipantFATERepo)(nil) + +// ErrParticipantExist means new participant cannot be created due to name conflicts +var ErrParticipantExist = errors.New("participant already exists") + +func (r *ParticipantFATERepo) Create(instance interface{}) error { + var count int64 + participant := instance.(*entity.ParticipantFATE) + db.Model(&entity.ParticipantFATE{}).Where("name = ? AND federation_uuid = ?", participant.Name, participant.FederationUUID).Count(&count) + if count > 0 { + return ErrParticipantExist + } + + if err := db.Create(participant).Error; err != nil { + return err + } + return nil +} + +func (r *ParticipantFATERepo) List() (interface{}, error) { + var participantList []entity.ParticipantFATE + if err := db.Find(&participantList).Error; err != nil { + return nil, err + } + return participantList, nil +} + +func (r *ParticipantFATERepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.ParticipantFATE{}).Error +} + +func (r *ParticipantFATERepo) GetByUUID(uuid string) (interface{}, error) { + participant := &entity.ParticipantFATE{} + if err := db.Where("uuid = ?", uuid).First(participant).Error; err != nil { + return nil, err + } + return participant, nil +} + +func (r *ParticipantFATERepo) ListByFederationUUID(federationUUID string) (interface{}, error) { + var participants []entity.ParticipantFATE + err := db.Where("federation_uuid = ?", federationUUID).Find(&participants).Error + if err != nil { + return 0, err + } + return participants, nil +} + +func (r *ParticipantFATERepo) ListByEndpointUUID(endpointUUID string) (interface{}, error) { + var participants []entity.ParticipantFATE + err := db.Where("endpoint_uuid = ?", endpointUUID).Find(&participants).Error + if err != nil { + return 0, err + } + return participants, nil +} + +func (r *ParticipantFATERepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantFATE) + return db.Model(&entity.ParticipantFATE{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *ParticipantFATERepo) UpdateDeploymentYAMLByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantFATE) + return db.Model(&entity.ParticipantFATE{}).Where("uuid = ?", participant.UUID). + Update("deployment_yaml", participant.DeploymentYAML).Error +} + +func (r *ParticipantFATERepo) UpdateInfoByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantFATE) + return db.Where("uuid = ?", participant.UUID). + Select("cluster_uuid", "status", "access_info", "cert_config", "extra_attribute", "ingress_info", "job_uuid"). + Updates(participant).Error +} + +func (r *ParticipantFATERepo) GetExchangeByFederationUUID(uuid string) (interface{}, error) { + participant := &entity.ParticipantFATE{} + if err := db.Where("federation_uuid = ? AND type = ?", uuid, entity.ParticipantFATETypeExchange).First(participant).Error; err != nil { + return nil, err + } + return participant, nil +} + +func (r *ParticipantFATERepo) IsExchangeCreatedByFederationUUID(uuid string) (bool, error) { + var count int64 + if err := db.Model(&entity.ParticipantFATE{}).Where("federation_uuid = ? AND type = ?", uuid, entity.ParticipantFATETypeExchange).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (r *ParticipantFATERepo) IsConflictedByFederationUUIDAndPartyID(federationUUID string, partyID int) (bool, error) { + var count int64 + err := db.Model(&entity.ParticipantFATE{}).Where("federation_uuid = ? AND party_id = ?", federationUUID, partyID).Count(&count).Error + return count > 0, err +} + +// InitTable makes sure the table is created in the db +func (r *ParticipantFATERepo) InitTable() { + if err := db.AutoMigrate(entity.ParticipantFATE{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/participate_openfl_repo.go b/server/infrastructure/gorm/participate_openfl_repo.go new file mode 100644 index 00000000..1e96daf4 --- /dev/null +++ b/server/infrastructure/gorm/participate_openfl_repo.go @@ -0,0 +1,127 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" +) + +// ParticipantOpenFLRepo implements repo.ParticipantOpenFLRepository interface +type ParticipantOpenFLRepo struct{} + +var _ repo.ParticipantOpenFLRepository = (*ParticipantOpenFLRepo)(nil) + +func (r *ParticipantOpenFLRepo) Create(instance interface{}) error { + var count int64 + participant := instance.(*entity.ParticipantOpenFL) + db.Model(&entity.ParticipantOpenFL{}).Where("name = ? AND federation_uuid = ?", participant.Name, participant.FederationUUID).Count(&count) + if count > 0 { + return ErrParticipantExist + } + + if err := db.Create(participant).Error; err != nil { + return err + } + return nil +} + +func (r *ParticipantOpenFLRepo) List() (interface{}, error) { + var participantList []entity.ParticipantOpenFL + if err := db.Find(&participantList).Error; err != nil { + return nil, err + } + return participantList, nil +} + +func (r *ParticipantOpenFLRepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.ParticipantOpenFL{}).Error +} + +func (r *ParticipantOpenFLRepo) GetByUUID(uuid string) (interface{}, error) { + participant := &entity.ParticipantOpenFL{} + if err := db.Where("uuid = ?", uuid).First(participant).Error; err != nil { + return nil, err + } + return participant, nil +} + +func (r *ParticipantOpenFLRepo) ListByFederationUUID(federationUUID string) (interface{}, error) { + var participants []entity.ParticipantOpenFL + err := db.Where("federation_uuid = ?", federationUUID).Find(&participants).Error + if err != nil { + return 0, err + } + return participants, nil +} + +func (r *ParticipantOpenFLRepo) ListByEndpointUUID(endpointUUID string) (interface{}, error) { + var participants []entity.ParticipantOpenFL + err := db.Where("endpoint_uuid = ?", endpointUUID).Find(&participants).Error + if err != nil { + return 0, err + } + return participants, nil +} + +func (r *ParticipantOpenFLRepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantOpenFL) + return db.Model(&entity.ParticipantOpenFL{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *ParticipantOpenFLRepo) UpdateDeploymentYAMLByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantOpenFL) + return db.Model(&entity.ParticipantOpenFL{}).Where("uuid = ?", participant.UUID). + Update("deployment_yaml", participant.DeploymentYAML).Error +} + +func (r *ParticipantOpenFLRepo) UpdateInfoByUUID(instance interface{}) error { + participant := instance.(*entity.ParticipantOpenFL) + return db.Where("uuid = ?", participant.UUID). + Select("endpoint_uuid", "cluster_uuid", "status", "access_info", "cert_config", "extra_attribute", "job_uuid"). + Updates(participant).Error +} + +func (r *ParticipantOpenFLRepo) IsDirectorCreatedByFederationUUID(uuid string) (bool, error) { + var count int64 + if err := db.Model(&entity.ParticipantOpenFL{}).Where("federation_uuid = ? AND type = ?", uuid, entity.ParticipantOpenFLTypeDirector).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (r *ParticipantOpenFLRepo) CountByTokenUUID(uuid string) (int, error) { + var count int64 + if err := db.Unscoped().Model(&entity.ParticipantOpenFL{}).Where("token_uuid = ?", uuid).Count(&count).Error; err != nil { + return 0, err + } + return int(count), nil +} + +func (r *ParticipantOpenFLRepo) GetDirectorByFederationUUID(uuid string) (interface{}, error) { + participant := &entity.ParticipantOpenFL{} + if err := db.Where("federation_uuid = ? AND type = ?", uuid, entity.ParticipantOpenFLTypeDirector).First(participant).Error; err != nil { + return nil, err + } + return participant, nil +} + +// InitTable makes sure the table is created in the db +func (r *ParticipantOpenFLRepo) InitTable() { + if err := db.AutoMigrate(entity.ParticipantOpenFL{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/registration_token_openfl_repo.go b/server/infrastructure/gorm/registration_token_openfl_repo.go new file mode 100644 index 00000000..6fdd0c1f --- /dev/null +++ b/server/infrastructure/gorm/registration_token_openfl_repo.go @@ -0,0 +1,83 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" +) + +// RegistrationTokenOpenFLRepo implements repo.RegistrationTokenRepository interface +type RegistrationTokenOpenFLRepo struct{} + +var _ repo.RegistrationTokenRepository = (*RegistrationTokenOpenFLRepo)(nil) + +// ErrTokenExist means new token cannot be created due to name conflicts +var ErrTokenExist = errors.New("token already exists") + +func (r *RegistrationTokenOpenFLRepo) Create(instance interface{}) error { + var count int64 + token := instance.(*entity.RegistrationTokenOpenFL) + db.Model(&entity.RegistrationTokenOpenFL{}).Where("name = ? AND federation_uuid = ?", token.Name, token.FederationUUID).Count(&count) + if count > 0 { + return ErrTokenExist + } + + if err := db.Create(token).Error; err != nil { + return err + } + return nil +} + +func (r *RegistrationTokenOpenFLRepo) ListByFederation(federationUUID string) (interface{}, error) { + var tokens []entity.RegistrationTokenOpenFL + err := db.Where("federation_uuid = ?", federationUUID).Find(&tokens).Error + if err != nil { + return 0, err + } + return tokens, nil +} + +func (r *RegistrationTokenOpenFLRepo) DeleteByFederation(federationUUID string) error { + return db.Unscoped().Where("federation_uuid = ?", federationUUID).Delete(&entity.RegistrationTokenOpenFL{}).Error +} + +func (r *RegistrationTokenOpenFLRepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.RegistrationTokenOpenFL{}).Error +} + +func (r *RegistrationTokenOpenFLRepo) GetByUUID(uuid string) (interface{}, error) { + token := &entity.RegistrationTokenOpenFL{} + if err := db.Where("uuid = ?", uuid).First(token).Error; err != nil { + return nil, err + } + return token, nil +} + +func (r *RegistrationTokenOpenFLRepo) LoadByTypeAndStr(instance interface{}) error { + token := instance.(*entity.RegistrationTokenOpenFL) + if err := db.Where("token_type = ? AND token_str = ?", token.TokenType, token.TokenStr).First(token).Error; err != nil { + return err + } + return nil +} + +// InitTable makes sure the table is created in the db +func (r *RegistrationTokenOpenFLRepo) InitTable() { + if err := db.AutoMigrate(entity.RegistrationTokenOpenFL{}); err != nil { + panic(err) + } +} diff --git a/server/infrastructure/gorm/user_repo.go b/server/infrastructure/gorm/user_repo.go new file mode 100644 index 00000000..554cc9a9 --- /dev/null +++ b/server/infrastructure/gorm/user_repo.go @@ -0,0 +1,124 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/server/domain/entity" + "github.com/FederatedAI/FedLCM/server/domain/repo" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "github.com/spf13/viper" + "golang.org/x/crypto/bcrypt" +) + +// UserRepo implements repo.UserRepository using gorm and PostgreSQL +type UserRepo struct{} + +// make sure UserRepo implements the repo.UserRepository interface +var _ repo.UserRepository = (*UserRepo)(nil) + +// ErrUserExist means new user cannot be created due to the existence of the same-name user +var ErrUserExist = errors.New("user already exists") + +// CreateUser creates a new user +func (r *UserRepo) CreateUser(instance interface{}) error { + // check name + var count int64 + newUser := instance.(*entity.User) + db.Model(&entity.User{}).Where("name = ?", newUser.Name).Count(&count) + if count > 0 { + return ErrUserExist + } + + // add data + if err := db.Create(newUser).Error; err != nil { + return err + } + + return nil +} + +// UpdatePasswordById changes the user's hashed password +func (r *UserRepo) UpdatePasswordById(id uint, hashedPassword string) error { + toUpdateUser := &entity.User{} + if err := db.Where("id = ?", id).First(&toUpdateUser).Error; err != nil { + return err + } + return db.Model(toUpdateUser).Update("password", hashedPassword).Error +} + +// UpdateByName changes the specified user's info +func (r *UserRepo) UpdateByName(updatedUser *entity.User) error { + return db.Model(&entity.User{}).Where("name = ?", updatedUser.Name).Updates(updatedUser).Error +} + +// GetByName returns the user info indexed by the name +func (r *UserRepo) GetByName(name string) (*entity.User, error) { + user := &entity.User{} + if err := db.Where("name = ?", name).First(&user).Error; err != nil { + return nil, err + } + return user, nil +} + +// LoadById loads the user info by id +func (r *UserRepo) LoadById(instance interface{}) error { + user := instance.(*entity.User) + return db.Where("id = ?", user.ID).First(&user).Error +} + +// LoadByName loads the user info by name +func (r *UserRepo) LoadByName(instance interface{}) error { + user := instance.(*entity.User) + return db.Where("name = ?", user.Name).First(&user).Error +} + +// InitTable makes sure the table is created in the db +func (r *UserRepo) InitTable() { + if err := db.AutoMigrate(entity.User{}); err != nil { + panic(err) + } +} + +// InitData inserts a default users information +func (r *UserRepo) InitData() { + + adminPassword := viper.GetString("lifecyclemanager.initial.admin.password") + if adminPassword == "" { + adminPassword = "admin" + } + hashedAdminPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + + // init 'admin' user + admin := &entity.User{ + UUID: uuid.NewV4().String(), + Name: "Admin", + Password: string(hashedAdminPassword), + } + + // if 'admin' exists, we keep using the original password + if err := r.CreateUser(admin); err != nil { + if err == ErrUserExist { + log.Info().Msgf("user: %s exists", admin.Name) + } else { + panic(err) + } + } + +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 00000000..a304801c --- /dev/null +++ b/server/main.go @@ -0,0 +1,181 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/server/api" + "github.com/FederatedAI/FedLCM/server/constants" + "github.com/FederatedAI/FedLCM/server/infrastructure/gorm" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/utils/logging" + "github.com/gin-contrib/logger" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + // swag API + _ "github.com/FederatedAI/FedLCM/server/docs" +) + +// main starts the API server +// @title lifecycle manager API service +// @version v1 +// @description backend APIs of lifecycle manager service +// @termsOfService http://swagger.io/terms/ +// @contact.name FedLCM team +// @BasePath /api/v1 +// @in header +func main() { + viper.AutomaticEnv() + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) + + log.Logger = log.Output( + zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }, + ).With().Caller().Stack().Logger().Level(zerolog.InfoLevel) + debugLog, _ := strconv.ParseBool(viper.GetString("lifecyclemanager.debug")) + if debugLog { + log.Logger = log.Logger.Level(zerolog.DebugLevel) + } + + if err := initDB(); err != nil { + panic(err) + } + r := createGinEngine() + initRouter(r) + + err := r.Run() + if err != nil { + log.Error().Err(err).Msg("gin run error, ") + return + } +} + +func createGinEngine() *gin.Engine { + r := gin.New() + + r.Use(gin.Recovery()) + r.Use(logger.SetLogger( + logger.WithUTC(true), + logger.WithLogger(logging.GetGinLogger))) + + return r +} + +func initDB() error { + var err error + + for i := 0; i < 3; i++ { + err = gorm.InitDB() + if err == nil { + return nil + } + time.Sleep(5 * time.Second) + } + + return fmt.Errorf("initialization failed: %s", err) +} + +func initRouter(r *gin.Engine) { + + v1 := r.Group("/api/" + constants.APIVersion) + { + v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + v1.GET("/status", func(c *gin.Context) { + c.JSON(200, gin.H{ + "msg": "The service is running", + "api_version": constants.APIVersion, + "source_commit": constants.Commit, + "source_branch": constants.Branch, + "build_time": constants.BuildTime, + "experiment_enabled": viper.GetBool("lifecyclemanager.experiment.enabled"), + }) + }) + + // user management + userRepo := &gorm.UserRepo{} + userRepo.InitTable() + userRepo.InitData() + // create authMiddleware before any other controllers + if err := api.CreateAuthMiddleware(userRepo); err != nil { + panic(err) + } + api.NewUserController(userRepo).Route(v1) + + // infra provider management + infraProviderKubernetesRepo := &gorm.InfraProviderKubernetesRepo{} + infraProviderKubernetesRepo.InitTable() + + // endpoint management + endpointKubeFATERepo := &gorm.EndpointKubeFATERepo{} + endpointKubeFATERepo.InitTable() + + // chart management + // TODO: replace this mock one with a complete implementation + chartRepo := &gorm.ChartMockRepo{} + api.NewChartController(chartRepo).Route(v1) + + // federation management + federationFATERepo := &gorm.FederationFATERepo{} + federationFATERepo.InitTable() + federationOpenFLRepo := &gorm.FederationOpenFLRepo{} + federationOpenFLRepo.InitTable() + + // participant management + participantFATETRepo := &gorm.ParticipantFATERepo{} + participantFATETRepo.InitTable() + participantOpenFLRepo := &gorm.ParticipantOpenFLRepo{} + participantOpenFLRepo.InitTable() + + // certificate management + certificateAuthorityRepo := &gorm.CertificateAuthorityRepo{} + certificateAuthorityRepo.InitTable() + certificateRepo := &gorm.CertificateRepo{} + certificateRepo.InitTable() + certificateBindingRepo := &gorm.CertificateBindingRepo{} + certificateBindingRepo.InitTable() + + // Event management + eventRepo := &gorm.EventRepo{} + eventRepo.InitTable() + + // Registration token management + registrationTokenOpenFLRepo := &gorm.RegistrationTokenOpenFLRepo{} + registrationTokenOpenFLRepo.InitTable() + + api.NewInfraProviderController(infraProviderKubernetesRepo, endpointKubeFATERepo).Route(v1) + api.NewEndpointController(infraProviderKubernetesRepo, endpointKubeFATERepo, participantFATETRepo, participantOpenFLRepo, eventRepo).Route(v1) + api.NewFederationController(infraProviderKubernetesRepo, endpointKubeFATERepo, + federationFATERepo, federationOpenFLRepo, chartRepo, participantFATETRepo, participantOpenFLRepo, certificateAuthorityRepo, + certificateRepo, certificateBindingRepo, registrationTokenOpenFLRepo, eventRepo).Route(v1) + + api.NewCertificateAuthorityController(certificateAuthorityRepo).Route(v1) + api.NewCertificateController(certificateAuthorityRepo, certificateRepo, certificateBindingRepo, participantFATETRepo, participantOpenFLRepo, federationFATERepo, federationOpenFLRepo).Route(v1) + api.NewEventController(eventRepo).Route(v1) + } +} diff --git a/site-portal/.env b/site-portal/.env new file mode 100644 index 00000000..99777b70 --- /dev/null +++ b/site-portal/.env @@ -0,0 +1,7 @@ +TAG=v0.1.0 + +SERVER_NAME=federatedai/site-portal-server +SERVER_IMG=${SERVER_NAME}:${TAG} + +FRONTEND_NAME=federatedai/site-portal-frontend +FRONTEND_IMG=${FRONTEND_NAME}:${TAG} \ No newline at end of file diff --git a/site-portal/.gitignore b/site-portal/.gitignore new file mode 100644 index 00000000..ce255b35 --- /dev/null +++ b/site-portal/.gitignore @@ -0,0 +1,15 @@ +*.cfg +*.idea/ +*.vscode/ +*output/ +*.todo +*.exe +dist/ +*.egg-info +*.test +.DS_Store +*.out +*.tgz +*.tar +/vendor/ +tls/cert diff --git a/site-portal/Makefile b/site-portal/Makefile new file mode 100644 index 00000000..4ca9d612 --- /dev/null +++ b/site-portal/Makefile @@ -0,0 +1,92 @@ +.PHONY: all clean format swag swag-bin server-unittest server frontend run + +TAG ?= v0.1.0 + +SERVER_NAME ?= federatedai/site-portal-server +SERVER_IMG ?= ${SERVER_NAME}:${TAG} + +FRONTEND_NAME ?= federatedai/site-portal-frontend +FRONTEND_IMG ?= ${FRONTEND_NAME}:${TAG} + +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +OUTPUT_DIR = output +ifeq ($(OS),Windows_NT) +BUILD_MODE = -buildmode=exe +OUTPUT_FILE = ${OUTPUT_DIR}/site-portal.exe +else +BUILD_MODE = +OUTPUT_FILE = ${OUTPUT_DIR}/site-portal +endif +OUTPUT_FRONTEND_FOLDER = ${OUTPUT_DIR}/frontend + +BRANCH ?= $(shell git symbolic-ref --short HEAD) +COMMIT ?= $(shell git log --pretty=format:'%h' -n 1) +NOW ?= $(shell date "+%Y-%m-%d %T UTC%z") + +LDFLAGS = "-X 'github.com/FederatedAI/FedLCM/site-portal/server/constants.Branch=$(BRANCH)' \ + -X 'github.com/FederatedAI/FedLCM/site-portal/server/constants.Commit=$(COMMIT)' \ + -X 'github.com/FederatedAI/FedLCM/site-portal/server/constants.BuildTime=$(NOW)' \ + -extldflags '-static'" + + +all: swag server frontend + +frontend: + rm -rf ${OUTPUT_FRONTEND_FOLDER} + mkdir -p ${OUTPUT_DIR} + cd frontend && npm run build --prod + cp -rf frontend/dist/Site-portal ${OUTPUT_FRONTEND_FOLDER} + +# Run go fmt & vet against code +format: + go fmt ./... + go vet ./... + +# Build manager binary +server: format + mkdir -p ${OUTPUT_DIR} + CGO_ENABLED=0 go build -a --ldflags ${LDFLAGS} -o ${OUTPUT_FILE} ${BUILD_MODE} server/main.go + +# Run server tests +server-unittest: format + go test ./... -coverprofile cover.out + + +run: format + env FRONTEND_DIR=${OUTPUT_FRONTEND_FOLDER} go run --ldflags ${LDFLAGS} ./server/main.go + +# Generate swag API file +swag: swag-bin + cd server && $(SWAG_BIN) init --parseDependency --parseInternal + +swag-bin: +ifeq (, $(shell which swag)) + @{ \ + set -e ;\ + SWAG_BIN_TMP_DIR=$$(mktemp -d) ;\ + cd $$SWAG_BIN_TMP_DIR ;\ + go mod init tmp ;\ + go get -u github.com/swaggo/swag/cmd/swag ;\ + rm -rf $$SWAG_BIN_TMP_DIR ;\ + } +SWAG_BIN=$(GOBIN)/swag +else +SWAG_BIN=$(shell which swag) +endif + +docker-build: + docker build . -t ${SERVER_IMG} -f make/server/Dockerfile --build-arg BRANCH=$(BRANCH) --build-arg COMMIT=$(COMMIT) + docker build . -t ${FRONTEND_IMG} -f make/frontend/Dockerfile + +docker-push: + docker push ${SERVER_IMG} + docker push ${FRONTEND_IMG} + +clean: + rm -rf ${OUTPUT_DIR} + rm -rf frontend/dist diff --git a/site-portal/README.md b/site-portal/README.md new file mode 100644 index 00000000..8a042936 --- /dev/null +++ b/site-portal/README.md @@ -0,0 +1,136 @@ +# FML Site Portal + +Managing FATE jobs from each site. + +## Build +``` +make all +``` +The generated deliverables are placed in the `output` folder. + +## Build & Run Docker Images +* Modify `.env` file to change the image names, and then +``` +set -a; source .env; set +a +make docker-build +``` +* Optionally push the image +``` +make docker-push +``` +* Start the service +``` +docker-compose up +``` +## Run Pre-built Images +* Modify `.env` file to change the image names, and then +``` +docker-compose pull +docker-compose up +``` + +> You may need to run `chmod a+w ./output/data/server/uploaded` to allow write permission to the uploaded data folder. Otherwise you may not be able to upload your local data. + +## Enable HTTPS using docker-compose +### Create Site Portal Server and Client certificates +The server cert is used for accepting connection from users and fml-manager and the client cert is used for connecting to fml-manager. + +**As an example, this guide uses StepCA CLI to sign and get the certificate.** + +1. Make sure your StepCA CA server is running. Put your CA `ca.crt` into `tls/cert` folder. +Use command below to get your CA cert. +``` +step ca root +``` +2. Then `cd` to `tls/cert` folder, run commands below to create certificates and keys (replace ``(e.g. 1.fate.org), ``(e.g. 8760h) with your site configuration): +``` +step ca certificate --san localhost server.crt server.key --not-after= + +step ca certificate client.crt client.key --not-after= +``` + +* For the server cert, the `localhost` SAN name is required because our sever may call itself via the localhost address. +* You can optionally add your other address and dns names as SANs in the command line. + +**If you want to use other methods to generate the certificates and keys** +* For server.crt server.key, make sure to include `` and `localhost`. + 1. If you have a usable FQDN, you can use it as your ``, and set `localhost` + 2. If you don't have a usable FQDN, set your `` with your preference. Then append with `localhost`. +* For client.crt client.key, make sure to include `` + +### Run Site Portal +* `cd` to the project root folder +``` +docker-compose -f docker-compose-https.yml up +``` +* Open Site Portal with URL `https://
:8443` + +## Deploy into Kubernetes +There are helms chart developed for installing Site Portal with the FATE exchange components together. Currently, it is used by the FedLCM service. Refer to the documents in the FedLCM. + +## Getting Started + +### 0. Prerequisites + +* A FATE instance using Spark and Pulsar as backend. +* An existing FML Manager service to help to coordinate between other Site Portals. + +### 1. Login Site Portal with predefined credentials +* If this portal is deployed by FedLCM service, then we can open it from the FedLCM's FATE cluster detail page. +* The predefined credentials for Site Portal are defined in the environment variables: + * `SITEPORTAL_INITIAL_ADMIN_PASSWORD` for user "Admin", by default, `admin`. + * `SITEPORTAL_INITIAL_USER_PASSWORD` for user "User", by default, `user`. +* You can change the password of you current user after login. Once changed, the above environment variable will no longer take effect. + +### 2. Configure site information +Firstly, in the "Site Configuration" page, we need to configure the basic information of the site. +* Party ID should be what is configured during deploying of the FATE cluster +* Site Address and Port is the exposed service address and port that can be accessed from other service, like from FML Manager. + * This depends on the type of the exposed service. When using FedLCM, such information can be found from the FedLCM page under the exposed service section. +* FML Manager endpoint: select to use `http` or `https`, then input the IP Address or FQDN for accessing the fml-manager + * For example,the completed url is: `http://:` +* FML Manager Server Name: When FML manager endpoint uses 'https', `Server Name` may be needed if 'FML Manager Endpoint' is an IP address. It's typical to use the Common Name or FQDN(if valid) of FML Manager as 'Server Name'. Site Portal uses 'FML Manager Server Name' to verify FML Manager's server certificate. If you leave it empty, server name will be 'FML Manager Endpoint' address. + * For example: `fmlmanager.server.fate.org` + * When FedLCM is used, such information can be found from the Federation's exchange detail page. +* FATE-Flow configuration is the service address of the fate flow service. + * When FedLCM is used, we can simply input the follow info: + * FATE-Flow address should be `fateflow` + * HTTP port should be `9380` + * If Site Portal is not deployed along with FATE cluster via KubeFATE, then users need to find the exposed ip and port of the FATE-Flow service. Currently it can only works with FATE v1.6.1 using Spark and Pulsar as backend. +* The Kubeflow configuration is for deploying horizontal models to KFServing system. They are optional and require an installation of MinIO and KFServing. Currently they are not used. +* After saving the configuration, click "register" button next to the FML Manager configuration section to register this service to the FML Manager. + * In the future, if we have changed some site settings, the connection status to FML Manager will change to not connected. We need to register again to update our new site information to FML Manager. + * This means if you want to disconnect ourselves from FML Manager, then you can clean the FML Manager endpoint settings and save again. + +### 3. Upload local data in the "data management" page +* CSV files can be uploaded to the system via this page. +* Site Portal needs to upload the data to the FATE cluster, so the "Upload Job Status" field of each data is the status of the "uploading to FATE" job. + * Only data in "Succeeded" status can be used in future FATE training and predicting jobs. + +### 4. Create project in the "project management" page +Once created, this project can be viewed as a "local" project. Its information will not be sent to FML Manager, until we invite other parties to join this project. + +### 5. Invite other parties +* Make sure other parties' FATE cluster and Site Portal are deployed and configured, in the same way above. +* In the participant management tab, view other registered participant and send invitation to others. +* The invited party's site portal page will list the invitation and users of that party can choose to join or reject. + +### 6. Associate data +* All parties can associate their local data into the current project from the data management page. +* When data association is changed, all participating parties should see the updated information. + +### 7. Start jobs +* In the "job management" tab, one can create new FATE job with other parties. +* Any joined party can initiate new jobs. +* We provide two modes to create FATE jobs: Drag-n-Drop and Json template. + +### 8. Work with trained models +* Models can be viewed in the "model management" tab in project or "model management" page in the main page. +* They can be used in "prediction" type of job. +* Currently the publish function will return error as FATE v1.6.1 does not support such operation. This is a placeholder for future integration with newer FATE versions. + +### 9. Other operations +* All parties can dismiss their own data association so it won't be used for futurre jobs. +* Project joining participant can leave the joined project if it no longer want to participate. +* Project managing participant can close the project if it is no longer need. +* The "User Management" page provides some configurations to set user permissions for accessing FATE Jupyter Notebook and FATEBoard. But currently it is not implemented yet. It is a placeholder for future integrations. diff --git a/site-portal/docker-compose-https.yml b/site-portal/docker-compose-https.yml new file mode 100644 index 00000000..43ec69e7 --- /dev/null +++ b/site-portal/docker-compose-https.yml @@ -0,0 +1,51 @@ +version: '3.5' + +services: + server: + image: "${SERVER_IMG}" + depends_on: + - postgres + restart: always + volumes: + - ./output/data/server/uploaded:/var/lib/site-portal/data/uploaded + - ./tls/cert:/var/lib/site-portal/cert + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: site_portal + POSTGRES_PASSWORD: site_portal + POSTGRES_DB: site_portal + SITEPORTAL_INITIAL_ADMIN_PASSWORD: admin + SITEPORTAL_INITIAL_USER_PASSWORD: user + SITEPORTAL_LOCALDATA_BASEDIR: /var/lib/site-portal/data/uploaded + SITEPORTAL_TLS_SERVER_CERT: /var/lib/site-portal/cert/server.crt + SITEPORTAL_TLS_SERVER_KEY: /var/lib/site-portal/cert/server.key + SITEPORTAL_TLS_CLIENT_CERT: /var/lib/site-portal/cert/client.crt + SITEPORTAL_TLS_CLIENT_KEY: /var/lib/site-portal/cert/client.key + SITEPORTAL_TLS_CA_CERT: /var/lib/site-portal/cert/ca.crt + SITEPORTAL_TLS_ENABLED: 'true' + SITEPORTAL_TLS_PORT: 8443 + SITEPORTAL_TLS_COMMON_NAME: 1.fate.org + + postgres: + image: postgres:13.3 + volumes: + - ./output/data/postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_PASSWORD: site_portal + POSTGRES_USER: site_portal + POSTGRES_DB: site_portal + + frontend: + image: "${FRONTEND_IMG}" + restart: always + depends_on: + - server + ports: + - "8443:8443" + volumes: + - ./tls/nginx.conf.template:/etc/nginx/conf.d/nginx.conf.template + - ./tls/cert:/var/lib/site-portal/cert \ No newline at end of file diff --git a/site-portal/docker-compose.yml b/site-portal/docker-compose.yml new file mode 100644 index 00000000..ad3ed6ab --- /dev/null +++ b/site-portal/docker-compose.yml @@ -0,0 +1,40 @@ +version: '3.5' + +services: + server: + image: "${SERVER_IMG}" + depends_on: + - postgres + restart: always + volumes: + - ./output/data/server/uploaded:/var/lib/site-portal/data/uploaded + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: site_portal + POSTGRES_PASSWORD: site_portal + POSTGRES_DB: site_portal + SITEPORTAL_INITIAL_ADMIN_PASSWORD: admin + SITEPORTAL_INITIAL_USER_PASSWORD: user + SITEPORTAL_LOCALDATA_BASEDIR: /var/lib/site-portal/data/uploaded + SITEPORTAL_TLS_ENABLED: 'false' + + postgres: + image: postgres:13.3 + volumes: + - ./output/data/postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: always + environment: + POSTGRES_PASSWORD: site_portal + POSTGRES_USER: site_portal + POSTGRES_DB: site_portal + + frontend: + image: "${FRONTEND_IMG}" + restart: always + depends_on: + - server + ports: + - "8080:8080" \ No newline at end of file diff --git a/site-portal/frontend/.browserslistrc b/site-portal/frontend/.browserslistrc new file mode 100644 index 00000000..427441dc --- /dev/null +++ b/site-portal/frontend/.browserslistrc @@ -0,0 +1,17 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/site-portal/frontend/.editorconfig b/site-portal/frontend/.editorconfig new file mode 100644 index 00000000..59d9a3a3 --- /dev/null +++ b/site-portal/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/site-portal/frontend/.gitignore b/site-portal/frontend/.gitignore new file mode 100644 index 00000000..de51f68a --- /dev/null +++ b/site-portal/frontend/.gitignore @@ -0,0 +1,45 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/site-portal/frontend/README.md b/site-portal/frontend/README.md new file mode 100644 index 00000000..572f861f --- /dev/null +++ b/site-portal/frontend/README.md @@ -0,0 +1,22 @@ +# Site Portal Frontend UI + +This frontend UI of Site Portal is based on Clarity and Angular. + +# Start a Dev Environment: +1. Run `npm install` under "frontend" directory. +2. create "proxy.config.json" file under "frontend" directory, and replace the URL of target with your available backend server. +``` + { + "/api/v1": { + "target": "http://localhost:8080", + "secure": false, + "changeOrigin": true, + "logLevel": "debug", + "headers": { + "Connection": "keep-alive" + } + } + } +``` +3. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +4. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. \ No newline at end of file diff --git a/site-portal/frontend/angular.json b/site-portal/frontend/angular.json new file mode 100644 index 00000000..d6ecc338 --- /dev/null +++ b/site-portal/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "Site-portal": { + "projectType": "application", + "schematics": { + "@schematics/angular:application": { + "strict": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/Site-portal", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/assets/siteportal-icon.png" + ], + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": true + }, + "styles": [ + "src/styles.css", + "node_modules/@clr/ui/clr-ui.min.css" + ], + "scripts": [ + "node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js", + "node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kb", + "maximumError": "8kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "Site-portal:build", + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "browserTarget": "Site-portal:build:production" + }, + "development": { + "browserTarget": "Site-portal:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "Site-portal:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + }, + "defaultProject": "Site-portal" +} diff --git a/site-portal/frontend/karma.conf.js b/site-portal/frontend/karma.conf.js new file mode 100644 index 00000000..708e39ea --- /dev/null +++ b/site-portal/frontend/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/Site-portal'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/site-portal/frontend/package-lock.json b/site-portal/frontend/package-lock.json new file mode 100644 index 00000000..7cd93634 --- /dev/null +++ b/site-portal/frontend/package-lock.json @@ -0,0 +1,13221 @@ +{ + "name": "Site-portal", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1200.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1200.5.tgz", + "integrity": "sha512-222VZ4OeaDK3vON8V5m+w15SRWfUs5uOb4H9ij/H9/6tyHD83uWfCDoOGg+ax4wJVdWEFJIS+Vn4ijGcZCq9WQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.0.5", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/build-angular": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-12.0.5.tgz", + "integrity": "sha512-rFbaAQPeuWM9KE9lK3J0sF6GB9nKF/s2Z7rtzKux7whGTF3Tlj8NHrcSxZTf4eBm3cnEpaiod1uPBQg8fUWD4w==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1200.5", + "@angular-devkit/build-optimizer": "0.1200.5", + "@angular-devkit/build-webpack": "0.1200.5", + "@angular-devkit/core": "12.0.5", + "@babel/core": "7.14.3", + "@babel/generator": "7.14.3", + "@babel/plugin-transform-async-to-generator": "7.13.0", + "@babel/plugin-transform-runtime": "7.14.3", + "@babel/preset-env": "7.14.2", + "@babel/runtime": "7.14.0", + "@babel/template": "7.12.13", + "@discoveryjs/json-ext": "0.5.2", + "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@ngtools/webpack": "12.0.5", + "ansi-colors": "4.1.1", + "babel-loader": "8.2.2", + "browserslist": "^4.9.1", + "cacache": "15.0.6", + "caniuse-lite": "^1.0.30001032", + "circular-dependency-plugin": "5.2.2", + "copy-webpack-plugin": "8.1.1", + "core-js": "3.12.0", + "critters": "0.0.10", + "css-loader": "5.2.4", + "css-minimizer-webpack-plugin": "3.0.0", + "find-cache-dir": "3.3.1", + "glob": "7.1.7", + "https-proxy-agent": "5.0.0", + "inquirer": "8.0.0", + "jest-worker": "26.6.2", + "karma-source-map-support": "1.4.0", + "less": "4.1.1", + "less-loader": "8.1.1", + "license-webpack-plugin": "2.3.19", + "loader-utils": "2.0.0", + "mini-css-extract-plugin": "1.5.1", + "minimatch": "3.0.4", + "open": "8.0.2", + "ora": "5.4.0", + "parse5-html-rewriting-stream": "6.0.1", + "postcss": "8.3.0", + "postcss-import": "14.0.1", + "postcss-loader": "5.2.0", + "postcss-preset-env": "6.7.0", + "raw-loader": "4.0.2", + "regenerator-runtime": "0.13.7", + "resolve-url-loader": "4.0.0", + "rimraf": "3.0.2", + "rxjs": "6.6.7", + "sass": "1.32.12", + "sass-loader": "11.0.1", + "semver": "7.3.5", + "source-map": "0.7.3", + "source-map-loader": "2.0.1", + "source-map-support": "0.5.19", + "style-loader": "2.0.0", + "stylus": "0.54.8", + "stylus-loader": "5.0.0", + "terser": "5.7.0", + "terser-webpack-plugin": "5.1.2", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "webpack": "5.39.1", + "webpack-dev-middleware": "4.1.0", + "webpack-dev-server": "3.11.2", + "webpack-merge": "5.7.3", + "webpack-subresource-integrity": "1.5.2" + } + }, + "@angular-devkit/build-optimizer": { + "version": "0.1200.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1200.5.tgz", + "integrity": "sha512-3XlDVVak3CfIgUjDZMoON7sxnI1vxhzEm2LvVg5yN98Q7ijnfykXiIzryEcplzTMTZwGNkem0361HDs1EX8zNg==", + "dev": true, + "requires": { + "source-map": "0.7.3", + "tslib": "2.2.0", + "typescript": "4.2.4" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1200.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1200.5.tgz", + "integrity": "sha512-cx8DVwOmogzeHS1dsZGWummwA3T1zqsJ6q9NIL0S0ZiOScZuz0j9zAOLjBkyCj5XexOFzHv3TcAVQyzJsi1tIw==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1200.5", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.0.5.tgz", + "integrity": "sha512-zVSQV+8/vjUjsUKGlj8Kf5LioA6AXJTGI0yhHW9q1dFX4dPpbW63k0R1UoIB2wJ0F/AbYVgpnPGPe9BBm2fvZA==", + "dev": true, + "requires": { + "ajv": "8.2.0", + "ajv-formats": "2.0.2", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + } + }, + "@angular-devkit/schematics": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.0.5.tgz", + "integrity": "sha512-iW3XuDHScr3TXuunlEjF5O01zBpwpLgfr1oEny8PvseFGDlHK4Nj8zNIoIn3Yg936aiFO4GJAC/UXsT8g5vKxQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.0.5", + "ora": "5.4.0", + "rxjs": "6.6.7" + } + }, + "@angular/animations": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-12.0.5.tgz", + "integrity": "sha512-BPdTCtgDJ9zNzHpuA6X3NmtzDiIt5SHZk840j0q3HCq6rP6C/oo2UnPT6w8YDOGMejDpWdHvTgXH4jVKDN1wCQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/cdk": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.1.1.tgz", + "integrity": "sha512-MJENa8qmfLAr6t59u1+mEC2YPbCn4n3vsY6k8fKyf+ILXwwGHWNZlYblaRMBjrF/crSx1Kd5vb30RCqIcNTGsA==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + } + } + }, + "@angular/cli": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-12.0.5.tgz", + "integrity": "sha512-MdgJ9DY3bWYsMFr9Xa+60gtVaYErhAE8ULGnUyI8zLMhWqrV1ZpJJ1WfP8pMQYx4HaKJIgx7wd8az7ZXAcF8hg==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1200.5", + "@angular-devkit/core": "12.0.5", + "@angular-devkit/schematics": "12.0.5", + "@schematics/angular": "12.0.5", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.3.1", + "ini": "2.0.0", + "inquirer": "8.0.0", + "jsonc-parser": "3.0.0", + "npm-package-arg": "8.1.2", + "npm-pick-manifest": "6.1.1", + "open": "8.0.2", + "ora": "5.4.0", + "pacote": "11.3.2", + "resolve": "1.20.0", + "rimraf": "3.0.2", + "semver": "7.3.5", + "symbol-observable": "4.0.0", + "uuid": "8.3.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "@angular/common": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.0.5.tgz", + "integrity": "sha512-jKiPjWVL3jXVKgXwINlsF5O0r9gX/mAoa5UVy57O8jcg+ENbH9LLSOikgiF/0HPxk2uvRV5OYmbBgOY1xT41kQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/compiler": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-12.0.5.tgz", + "integrity": "sha512-G255aP4hhQ04hSQ1/JgiKNeTzRCuLgK220lJXkHubpu+f67os5LArhFNxZaUC/EVa/sloOT7Fo5tn8C5gy7iLA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/compiler-cli": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-12.0.5.tgz", + "integrity": "sha512-XBZWU2S7N2kvQJK0H5KyLHiLVhYJrjh3NtbVBv67sCY9Ft8fv2jjbozTgXqeoYZ1xAxcZ2ZAB0n5SkhmY75Mow==", + "dev": true, + "requires": { + "@babel/core": "^7.8.6", + "@babel/types": "^7.8.6", + "canonical-path": "1.0.0", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "source-map": "^0.6.1", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.1.0", + "yargs": "^16.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "@angular/core": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.0.5.tgz", + "integrity": "sha512-uVYsZa1VqVw8vNcjUYgfjXBc1M3WaxLXoLnCsDvutJiln4csa8Yw8p7RqQSrZ6AgQ8o2LHD2JJ5kus3EDMwfcA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/forms": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-12.0.5.tgz", + "integrity": "sha512-Ew/fGPTsywoYnm6DFPA/DyLl4Sb+1/uzpledrbxUHzaSKIrnXFrjQiUTmsbbq+8qono3JzbUIblqH1DrNThYiA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/localize": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-12.0.5.tgz", + "integrity": "sha512-CRyXsNJYV7TJBsbG/Sn6lW9qMQCa+lw5SSNKHvnmfCTyd5p3DV8AdjOYkyWM5tfB4wg/aOc4C1ynDom4TmKv+w==", + "requires": { + "@babel/core": "7.8.3", + "glob": "7.1.7", + "yargs": "^16.2.0" + }, + "dependencies": { + "@babel/core": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz", + "integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helpers": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } + } + }, + "@angular/platform-browser": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.0.5.tgz", + "integrity": "sha512-MLioK9gcdZOKINE8OmIHZICRnFySaWyCBeVbHS4Z4Vxgr+E2S2eO1IxBmGyQpJq1wDPBP9A/9LVLMUNbHu4Cdw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.0.5.tgz", + "integrity": "sha512-sYkOJxXj4vEZICT2oODkQF9wNaKoScSkiw2ooBYN0UX02mFKlWKa9vkzp6JmN1EF8YOWF0JnRqBPAi1WbOnAMw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@angular/router": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-12.0.5.tgz", + "integrity": "sha512-2jiaT+OxCmJbeJ0MTPmIHBsTFLysenvPZteozYsjcmUo9mOzJHAjqHLJvTC+Ri+E9xvnplh+8BPETRleV1pAFw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/compat-data": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", + "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==", + "dev": true + }, + "@babel/core": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.3", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.2", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.3", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", + "requires": { + "@babel/types": "^7.14.2", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", + "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz", + "integrity": "sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", + "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz", + "integrity": "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-member-expression-to-functions": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", + "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", + "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz", + "integrity": "sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + } + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", + "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", + "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-simple-access": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", + "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz", + "integrity": "sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-wrap-function": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", + "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", + "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz", + "integrity": "sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==" + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz", + "integrity": "sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + } + } + }, + "@babel/helpers": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz", + "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==", + "requires": { + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + } + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-proposal-optional-chaining": "^7.14.5" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", + "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz", + "integrity": "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz", + "integrity": "sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", + "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", + "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", + "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", + "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz", + "integrity": "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz", + "integrity": "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", + "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.14.5" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", + "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz", + "integrity": "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz", + "integrity": "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-create-class-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", + "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", + "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz", + "integrity": "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-remap-async-to-generator": "^7.13.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", + "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz", + "integrity": "sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz", + "integrity": "sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", + "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", + "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", + "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", + "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz", + "integrity": "sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", + "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", + "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", + "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", + "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz", + "integrity": "sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", + "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz", + "integrity": "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz", + "integrity": "sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", + "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", + "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", + "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.3.tgz", + "integrity": "sha512-t960xbi8wpTFE623ef7sd+UpEC5T6EEguQlTBJDEO05+XwnIWVfuqLw/vdLWY6IdFmtZE+65CZAfByT39zRpkg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", + "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz", + "integrity": "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", + "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", + "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", + "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz", + "integrity": "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", + "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/preset-env": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.2.tgz", + "integrity": "sha512-7dD7lVT8GMrE73v4lvDEb85cgcQhdES91BSD7jS/xjC6QY8PnRhux35ac+GCpbiRhp8crexBvZZqnaL6VrY8TQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.14.0", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-async-generator-functions": "^7.14.2", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-class-static-block": "^7.13.11", + "@babel/plugin-proposal-dynamic-import": "^7.14.2", + "@babel/plugin-proposal-export-namespace-from": "^7.14.2", + "@babel/plugin-proposal-json-strings": "^7.14.2", + "@babel/plugin-proposal-logical-assignment-operators": "^7.14.2", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.2", + "@babel/plugin-proposal-numeric-separator": "^7.14.2", + "@babel/plugin-proposal-object-rest-spread": "^7.14.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.14.2", + "@babel/plugin-proposal-optional-chaining": "^7.14.2", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "^7.14.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.13", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.0", + "@babel/plugin-syntax-top-level-await": "^7.12.13", + "@babel/plugin-transform-arrow-functions": "^7.13.0", + "@babel/plugin-transform-async-to-generator": "^7.13.0", + "@babel/plugin-transform-block-scoped-functions": "^7.12.13", + "@babel/plugin-transform-block-scoping": "^7.14.2", + "@babel/plugin-transform-classes": "^7.14.2", + "@babel/plugin-transform-computed-properties": "^7.13.0", + "@babel/plugin-transform-destructuring": "^7.13.17", + "@babel/plugin-transform-dotall-regex": "^7.12.13", + "@babel/plugin-transform-duplicate-keys": "^7.12.13", + "@babel/plugin-transform-exponentiation-operator": "^7.12.13", + "@babel/plugin-transform-for-of": "^7.13.0", + "@babel/plugin-transform-function-name": "^7.12.13", + "@babel/plugin-transform-literals": "^7.12.13", + "@babel/plugin-transform-member-expression-literals": "^7.12.13", + "@babel/plugin-transform-modules-amd": "^7.14.2", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/plugin-transform-modules-systemjs": "^7.13.8", + "@babel/plugin-transform-modules-umd": "^7.14.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.13", + "@babel/plugin-transform-new-target": "^7.12.13", + "@babel/plugin-transform-object-super": "^7.12.13", + "@babel/plugin-transform-parameters": "^7.14.2", + "@babel/plugin-transform-property-literals": "^7.12.13", + "@babel/plugin-transform-regenerator": "^7.13.15", + "@babel/plugin-transform-reserved-words": "^7.12.13", + "@babel/plugin-transform-shorthand-properties": "^7.12.13", + "@babel/plugin-transform-spread": "^7.13.0", + "@babel/plugin-transform-sticky-regex": "^7.12.13", + "@babel/plugin-transform-template-literals": "^7.13.0", + "@babel/plugin-transform-typeof-symbol": "^7.12.13", + "@babel/plugin-transform-unicode-escapes": "^7.12.13", + "@babel/plugin-transform-unicode-regex": "^7.12.13", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.14.2", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", + "core-js-compat": "^3.9.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", + "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.14.7", + "@babel/types": "^7.14.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + }, + "@cds/city": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cds/city/-/city-1.1.0.tgz", + "integrity": "sha512-S9K+Q39BGOghyLHmR0Wdcmu1i1noSUk8HcvMj+3IaohZw02WFd99aPTQDHJeseXrXZP3CNovaSlePI0R11NcFg==", + "optional": true + }, + "@cds/core": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@cds/core/-/core-5.4.1.tgz", + "integrity": "sha512-7RhtrMx5UM6xCU+2l9T7E/MBZ+nDqGGyzOLz9EhscKjurIJQrGTPAIwgomJNZ22W2Gw+S36+v/R62y4uStQFTg==", + "requires": { + "@cds/city": "^1.1.0", + "@types/resize-observer-browser": "^0.1.5", + "lit": "2.0.0-rc.2", + "normalize.css": "^8.0.1", + "ramda": "^0.27.1", + "tslib": "^2.2.0" + } + }, + "@clr/angular": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@clr/angular/-/angular-5.4.1.tgz", + "integrity": "sha512-3hsJE4+aVHSNh/jhc+UZPScD97SeOOEgTKL270wazojTdUl7MacJyclgYxekzTS+DWWfiT1Fd3v7ccKLD17sQA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@clr/ui": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@clr/ui/-/ui-5.4.1.tgz", + "integrity": "sha512-E03VDj43moarX2mp4U48h9Nrx9N6foDPjzqTFqKvjFaAIzEs/5GM8cNlo6EizVBV7y3+MwoM72bT5XJTygRL6w==" + }, + "@csstools/convert-colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", + "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", + "dev": true + }, + "@discoveryjs/json-ext": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz", + "integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jsdevtools/coverage-istanbul-loader": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", + "dev": true, + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.7.0" + } + }, + "@lit/reactive-element": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz", + "integrity": "sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==" + }, + "@ng-bootstrap/ng-bootstrap": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-10.0.0.tgz", + "integrity": "sha512-Sz+QaxjuyJYJ+zyUbf0TevgcgVesCPQiiFiggEzxKjzY5R+Hvq3YgryLdXf2r/ryePL+C3FXCcmmKpTM5bfczQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@ngtools/webpack": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.0.5.tgz", + "integrity": "sha512-yoKK6qhEm1iWnniz50xzOBqa3U1iUrjZs+SpLOXRZFonpwObz8j4zraR231K4uV4kXcX40qorYk9iOf+ljG4JQ==", + "dev": true, + "requires": { + "enhanced-resolve": "5.7.0" + } + }, + "@ngx-translate/core": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-13.0.0.tgz", + "integrity": "sha512-+tzEp8wlqEnw0Gc7jtVRAJ6RteUjXw6JJR4O65KlnxOmJrCGPI0xjV/lKRnQeU0w4i96PQs/jtpL921Wrb7PWg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngx-translate/http-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-6.0.0.tgz", + "integrity": "sha512-LCekn6qCbeXWlhESCxU1rAbZz33WzDG0lI7Ig0pYC1o5YxJWrkU9y3Y4tNi+jakQ7R6YhTR2D3ox6APxDtA0wA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", + "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/git": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", + "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", + "dev": true, + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz", + "integrity": "sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg==", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz", + "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==", + "dev": true, + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/run-script": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.5.tgz", + "integrity": "sha512-NQspusBCpTjNwNRFMtz2C5MxoxyzlbuJ4YEhxAKrIonTiirKDtatsZictx9RgamQIx6+QuHMNmPl0wQdoESs9A==", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "infer-owner": "^1.0.4", + "node-gyp": "^7.1.0", + "read-package-json-fast": "^2.0.1" + } + }, + "@schematics/angular": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.0.5.tgz", + "integrity": "sha512-gMT66T33az+uGLDSc7UkJVg+vloPeTpQNgWddBVGnW/Lkl1tGaWUxyqUJAp8AvusPNU+NCP+ZFB3qUm+pc7tCg==", + "dev": true, + "requires": { + "@angular-devkit/core": "12.0.5", + "@angular-devkit/schematics": "12.0.5", + "jsonc-parser": "3.0.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@trysound/sax": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.1.1.tgz", + "integrity": "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==", + "dev": true + }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==", + "dev": true + }, + "@types/cors": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", + "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==", + "dev": true + }, + "@types/d3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.0.0.tgz", + "integrity": "sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "@types/d3-array": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.1.tgz", + "integrity": "sha512-D/G7oG0czeszALrkdUiV68CDiHDxXf+M2mLVqAyKktGd12VKQQljj1sHJGBKjcK4jRH1biBd6ZPQPHpJ0mNa0w==", + "dev": true + }, + "@types/d3-axis": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz", + "integrity": "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-brush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz", + "integrity": "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==", + "dev": true + }, + "@types/d3-collection": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.10.tgz", + "integrity": "sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==", + "dev": true + }, + "@types/d3-color": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz", + "integrity": "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==", + "dev": true + }, + "@types/d3-contour": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz", + "integrity": "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==", + "dev": true, + "requires": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "@types/d3-delaunay": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz", + "integrity": "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==", + "dev": true + }, + "@types/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==", + "dev": true + }, + "@types/d3-drag": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz", + "integrity": "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-dsv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz", + "integrity": "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==", + "dev": true + }, + "@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==", + "dev": true + }, + "@types/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==", + "dev": true, + "requires": { + "@types/d3-dsv": "*" + } + }, + "@types/d3-force": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz", + "integrity": "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==", + "dev": true + }, + "@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "dev": true + }, + "@types/d3-geo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz", + "integrity": "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, + "@types/d3-hierarchy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz", + "integrity": "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==", + "dev": true + }, + "@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dev": true, + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==", + "dev": true + }, + "@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==", + "dev": true + }, + "@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==", + "dev": true + }, + "@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==", + "dev": true + }, + "@types/d3-scale": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.1.tgz", + "integrity": "sha512-GDuXcRcR6mKcpUVMhPNttpOzHi2dP6YcDqLZYSZHgwTZ+sfCa8e9q0VEBwZomblAPNMYpVqxojnSyIEb4s/Pwg==", + "dev": true, + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==", + "dev": true + }, + "@types/d3-selection": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz", + "integrity": "sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA==", + "dev": true + }, + "@types/d3-shape": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz", + "integrity": "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==", + "dev": true, + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "dev": true + }, + "@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==", + "dev": true + }, + "@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==", + "dev": true + }, + "@types/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==", + "dev": true, + "requires": { + "@types/d3-selection": "*" + } + }, + "@types/d3-voronoi": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz", + "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==", + "dev": true + }, + "@types/d3-zoom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz", + "integrity": "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==", + "dev": true, + "requires": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "@types/dagre": { + "version": "0.7.46", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.46.tgz", + "integrity": "sha512-ku3y+F8sPqmiB5Ugl22NpukI2dLAViJiWwdtueXLeuF4fxZozl+bytPSFVlLu/gDgaKiwobu3LBXXRWktIMiIA==", + "dev": true + }, + "@types/dagre-d3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@types/dagre-d3/-/dagre-d3-0.6.3.tgz", + "integrity": "sha512-qPd9DtaIbZArKqvEXXHrY1uljoFVQs812aIMCzJPX5TulVz/jEyqD05SJT9Oaan8abszUdqUjlXom9aZvLWpew==", + "dev": true, + "requires": { + "@types/d3": "^5", + "@types/dagre": "*", + "@types/graphlib": "*" + }, + "dependencies": { + "@types/d3": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.16.4.tgz", + "integrity": "sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==", + "dev": true, + "requires": { + "@types/d3-array": "^1", + "@types/d3-axis": "^1", + "@types/d3-brush": "^1", + "@types/d3-chord": "^1", + "@types/d3-collection": "*", + "@types/d3-color": "^1", + "@types/d3-contour": "^1", + "@types/d3-dispatch": "^1", + "@types/d3-drag": "^1", + "@types/d3-dsv": "^1", + "@types/d3-ease": "^1", + "@types/d3-fetch": "^1", + "@types/d3-force": "^1", + "@types/d3-format": "^1", + "@types/d3-geo": "^1", + "@types/d3-hierarchy": "^1", + "@types/d3-interpolate": "^1", + "@types/d3-path": "^1", + "@types/d3-polygon": "^1", + "@types/d3-quadtree": "^1", + "@types/d3-random": "^1", + "@types/d3-scale": "^2", + "@types/d3-scale-chromatic": "^1", + "@types/d3-selection": "^1", + "@types/d3-shape": "^1", + "@types/d3-time": "^1", + "@types/d3-time-format": "^2", + "@types/d3-timer": "^1", + "@types/d3-transition": "^1", + "@types/d3-voronoi": "*", + "@types/d3-zoom": "^1" + } + }, + "@types/d3-array": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.9.tgz", + "integrity": "sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==", + "dev": true + }, + "@types/d3-axis": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.16.tgz", + "integrity": "sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==", + "dev": true, + "requires": { + "@types/d3-selection": "^1" + } + }, + "@types/d3-brush": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.5.tgz", + "integrity": "sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==", + "dev": true, + "requires": { + "@types/d3-selection": "^1" + } + }, + "@types/d3-chord": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.11.tgz", + "integrity": "sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==", + "dev": true + }, + "@types/d3-color": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.2.tgz", + "integrity": "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==", + "dev": true + }, + "@types/d3-contour": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.3.tgz", + "integrity": "sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==", + "dev": true, + "requires": { + "@types/d3-array": "^1", + "@types/geojson": "*" + } + }, + "@types/d3-dispatch": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz", + "integrity": "sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==", + "dev": true + }, + "@types/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==", + "dev": true, + "requires": { + "@types/d3-selection": "^1" + } + }, + "@types/d3-dsv": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.1.tgz", + "integrity": "sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA==", + "dev": true + }, + "@types/d3-ease": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-1.0.10.tgz", + "integrity": "sha512-fMFTCzd8DOwruE9zlu2O8ci5ct+U5jkGcDS+cH+HCidnJlDs0MZ+TuSVCFtEzh4E5MasItwy+HvgoFtxPHa5Cw==", + "dev": true + }, + "@types/d3-fetch": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-1.2.2.tgz", + "integrity": "sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==", + "dev": true, + "requires": { + "@types/d3-dsv": "^1" + } + }, + "@types/d3-force": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz", + "integrity": "sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==", + "dev": true + }, + "@types/d3-format": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz", + "integrity": "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==", + "dev": true + }, + "@types/d3-geo": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.3.tgz", + "integrity": "sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA==", + "dev": true, + "requires": { + "@types/geojson": "*" + } + }, + "@types/d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==", + "dev": true + }, + "@types/d3-interpolate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz", + "integrity": "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==", + "dev": true, + "requires": { + "@types/d3-color": "^1" + } + }, + "@types/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==", + "dev": true + }, + "@types/d3-polygon": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-1.0.8.tgz", + "integrity": "sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==", + "dev": true + }, + "@types/d3-quadtree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz", + "integrity": "sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==", + "dev": true + }, + "@types/d3-random": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.3.tgz", + "integrity": "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==", + "dev": true + }, + "@types/d3-scale": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.6.tgz", + "integrity": "sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==", + "dev": true, + "requires": { + "@types/d3-time": "^1" + } + }, + "@types/d3-scale-chromatic": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz", + "integrity": "sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==", + "dev": true + }, + "@types/d3-selection": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.3.tgz", + "integrity": "sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==", + "dev": true + }, + "@types/d3-shape": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz", + "integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==", + "dev": true, + "requires": { + "@types/d3-path": "^1" + } + }, + "@types/d3-time": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz", + "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==", + "dev": true + }, + "@types/d3-time-format": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz", + "integrity": "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==", + "dev": true + }, + "@types/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==", + "dev": true + }, + "@types/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==", + "dev": true, + "requires": { + "@types/d3-selection": "^1" + } + }, + "@types/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q==", + "dev": true, + "requires": { + "@types/d3-interpolate": "^1", + "@types/d3-selection": "^1" + } + } + } + }, + "@types/eslint": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.13.tgz", + "integrity": "sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", + "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz", + "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==", + "dev": true + }, + "@types/file-saver": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.3.tgz", + "integrity": "sha512-MBIou8pd/41jkff7s97B47bc9+p0BszqqDJsO51yDm49uUxeKzrfuNl5fSLC6BpLEWKA8zlwyqALVmXrFwoBHQ==", + "dev": true + }, + "@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "dev": true + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@types/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-8nbbyD3zABRA9ePoBgAl2ym8cIwKQXTfv1gaIRTdY99yEOCaHfmjBeRp+BIemS8NtOqoWK7mfzWxjNrxLK3T5w==", + "dev": true + }, + "@types/jasmine": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz", + "integrity": "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==", + "dev": true + }, + "@types/jquery": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.9.tgz", + "integrity": "sha512-B8pDk+sH/tSv/HKdx6EQER6BfUOb2GtKs0LOmozziS4h7cbe8u/eYySfUAeTwD+J09SqV3man7AMWIA5mgzCBA==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", + "dev": true + }, + "@types/node": { + "version": "12.20.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.15.tgz", + "integrity": "sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/resize-observer-browser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz", + "integrity": "sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==", + "optional": true + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/trusted-types": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", + "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==" + }, + "@types/webpack-sources": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", + "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@webassemblyjs/ast": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", + "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", + "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", + "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", + "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", + "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", + "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", + "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", + "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", + "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", + "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", + "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/helper-wasm-section": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-opt": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "@webassemblyjs/wast-printer": "1.11.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", + "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", + "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", + "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", + "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webcomponents/webcomponentsjs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.5.0.tgz", + "integrity": "sha512-C0l51MWQZ9kLzcxOZtniOMohpIFdCLZum7/TEHv3XWFc1Fvt5HCpbSX84x8ltka/JuNKcuiDnxXFkiB2gaePcg==" + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz", + "integrity": "sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.2.0.tgz", + "integrity": "sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-formats": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.0.2.tgz", + "integrity": "sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "babel-loader": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", + "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.2", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.3.tgz", + "integrity": "sha512-rCOFzEIJpJEAU14XCcV/erIf/wZQMmMT5l5vXOpL5uoznyOGfDIjPj6FVytMvtzaKSTSVKouOCTPJ5OMUZH30g==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2", + "core-js-compat": "^3.14.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", + "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.6.tgz", + "integrity": "sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001240", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001240.tgz", + "integrity": "sha512-nb8mDzfMdxBDN7ZKx8chWafAdBp5DAAlpWvNyUGe5tcDWd838zpzDN3Rah9cjCqhfOKkrvx40G2SDtP0qiWX/w==", + "dev": true + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "circular-dependency-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", + "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colord": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.0.1.tgz", + "integrity": "sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA==", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-anything": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz", + "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==", + "dev": true, + "requires": { + "is-what": "^3.12.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz", + "integrity": "sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.5", + "glob-parent": "^5.1.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "core-js": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.12.0.tgz", + "integrity": "sha512-SaMnchL//WwU2Ot1hhkPflE8gzo7uq1FGvUJ8GKmi3TOU7rGTHIU+eir1WGf6qOtTyxdfdcp10yPdGZ59sQ3hw==", + "dev": true + }, + "core-js-compat": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.15.1.tgz", + "integrity": "sha512-xGhzYMX6y7oEGQGAJmP2TmtBLvR4nZmRGEcFa3ubHOq5YEp51gGN9AovVa0AoujGZIq+Wm6dISiYyGNfdflYww==", + "dev": true, + "requires": { + "browserslist": "^4.16.6", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "critters": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.10.tgz", + "integrity": "sha512-p5VKhP1803+f+0Jq5P03w1SbiHtpAKm+1EpJHkiPxQPq0Vu9QLZHviJ02GRrWi0dlcJqrmzMWInbwp4d22RsGw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "css": "^3.0.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "pretty-bytes": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-blank-pseudo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", + "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "css-color-names": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", + "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", + "dev": true + }, + "css-declaration-sorter": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz", + "integrity": "sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==", + "dev": true, + "requires": { + "timsort": "^0.3.0" + } + }, + "css-has-pseudo": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", + "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0-rc.4" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "css-loader": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.4.tgz", + "integrity": "sha512-OFYGyINCKkdQsTrSYxzGSFnGS4gNjcXkKkQgWxK138jgnPt+lepxdjSZNc8sHAl5vP3DhsJUxufWIjOwI8PMMw==", + "dev": true, + "requires": { + "camelcase": "^6.2.0", + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.10", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "css-minimizer-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-yIrqG0pPphR1RoNx2wDxYmxRf2ubRChLDXxv7ccipEm5bRKsZRYp8n+2peeXehtTF5s3yNxlqsdz3WQOsAgUkw==", + "dev": true, + "requires": { + "cssnano": "^5.0.0", + "jest-worker": "^26.3.0", + "p-limit": "^3.0.2", + "postcss": "^8.2.9", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-parse": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", + "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=", + "dev": true, + "requires": { + "css": "^2.0.0" + }, + "dependencies": { + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + } + } + }, + "css-prefers-color-scheme": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", + "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", + "dev": true + }, + "cssdb": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", + "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.6.tgz", + "integrity": "sha512-NiaLH/7yqGksFGsFNvSRe2IV/qmEBAeDE64dYeD8OBrgp6lE8YoMeQJMtsv5ijo6MPyhuoOvFhI94reahBRDkw==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "cssnano-preset-default": "^5.1.3", + "is-resolvable": "^1.1.0" + } + }, + "cssnano-preset-default": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.3.tgz", + "integrity": "sha512-qo9tX+t4yAAZ/yagVV3b+QBKeLklQbmgR3wI7mccrDcR+bEk9iHgZN1E7doX68y9ThznLya3RDmR+nc7l6/2WQ==", + "dev": true, + "requires": { + "css-declaration-sorter": "^6.0.3", + "cssnano-utils": "^2.0.1", + "postcss-calc": "^8.0.0", + "postcss-colormin": "^5.2.0", + "postcss-convert-values": "^5.0.1", + "postcss-discard-comments": "^5.0.1", + "postcss-discard-duplicates": "^5.0.1", + "postcss-discard-empty": "^5.0.1", + "postcss-discard-overridden": "^5.0.1", + "postcss-merge-longhand": "^5.0.2", + "postcss-merge-rules": "^5.0.2", + "postcss-minify-font-values": "^5.0.1", + "postcss-minify-gradients": "^5.0.1", + "postcss-minify-params": "^5.0.1", + "postcss-minify-selectors": "^5.1.0", + "postcss-normalize-charset": "^5.0.1", + "postcss-normalize-display-values": "^5.0.1", + "postcss-normalize-positions": "^5.0.1", + "postcss-normalize-repeat-style": "^5.0.1", + "postcss-normalize-string": "^5.0.1", + "postcss-normalize-timing-functions": "^5.0.1", + "postcss-normalize-unicode": "^5.0.1", + "postcss-normalize-url": "^5.0.2", + "postcss-normalize-whitespace": "^5.0.1", + "postcss-ordered-values": "^5.0.2", + "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-transforms": "^5.0.1", + "postcss-svgo": "^5.0.2", + "postcss-unique-selectors": "^5.0.1" + } + }, + "cssnano-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "dev": true + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "requires": { + "css-tree": "^1.1.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "requires": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.760", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.760.tgz", + "integrity": "sha512-XPKwjX6pHezJWB4FLVuSil9gGmU6XYl27ahUwEHODXF4KjCEB8RuIT05MkU1au2Tdye57o49yY0uCMK+bwUt+A==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", + "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "dev": true + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "dev": true, + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, + "enhanced-resolve": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", + "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", + "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "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" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz", + "integrity": "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "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" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + }, + "inquirer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.0.0.tgz", + "integrity": "sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.6", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + }, + "dependencies": { + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + } + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jasmine-core": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz", + "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", + "dev": true + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "karma": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.4.tgz", + "integrity": "sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==", + "dev": true, + "requires": { + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "colors": "^1.4.0", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.3.0", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^3.1.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.28", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, + "karma-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.0.3.tgz", + "integrity": "sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.1", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "minimatch": "^3.0.4" + } + }, + "karma-jasmine": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", + "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", + "dev": true, + "requires": { + "jasmine-core": "^3.6.0" + } + }, + "karma-jasmine-html-reporter": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz", + "integrity": "sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw==", + "dev": true + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "dev": true + }, + "less": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.1.tgz", + "integrity": "sha512-w09o8tZFPThBscl5d0Ggp3RcrKIouBoQscnOMgFH3n5V3kN/CXGHNfCkRPtxJk6nKryDXaV9aHLK55RXuH4sAw==", + "dev": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "less-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-8.1.1.tgz", + "integrity": "sha512-K93jJU7fi3n6rxVvzp8Cb88Uy9tcQKfHlkoezHwKILXhlNYiRQl4yowLIkQqmBXOH/5I8yoKiYeIf781HGkW9g==", + "dev": true, + "requires": { + "klona": "^2.0.4" + } + }, + "license-webpack-plugin": { + "version": "2.3.19", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz", + "integrity": "sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA==", + "dev": true, + "requires": { + "@types/webpack-sources": "^0.1.5", + "webpack-sources": "^1.2.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "lit": { + "version": "2.0.0-rc.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0-rc.2.tgz", + "integrity": "sha512-BOCuoJR04WaTV8UqTKk09cNcQA10Aq2LCcBOiHuF7TzWH5RNDsbCBP5QM9sLBSotGTXbDug/gFO08jq6TbyEtw==", + "requires": { + "@lit/reactive-element": "^1.0.0-rc.2", + "lit-element": "^3.0.0-rc.2", + "lit-html": "^2.0.0-rc.3" + } + }, + "lit-element": { + "version": "3.0.0-rc.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0-rc.2.tgz", + "integrity": "sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==", + "requires": { + "@lit/reactive-element": "^1.0.0-rc.2", + "lit-html": "^2.0.0-rc.3" + } + }, + "lit-html": { + "version": "2.0.0-rc.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0-rc.3.tgz", + "integrity": "sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==", + "requires": { + "@types/trusted-types": "^1.0.1" + } + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log4js": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", + "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", + "dev": true, + "requires": { + "date-format": "^3.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.1", + "rfdc": "^1.1.4", + "streamroller": "^2.2.4" + } + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-fetch-happen": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz", + "integrity": "sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==", + "dev": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.0.5", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^5.0.0", + "ssri": "^8.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + } + } + }, + "memfs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz", + "integrity": "sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==", + "dev": true, + "requires": { + "fs-monkey": "1.0.3" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dev": true, + "requires": { + "mime-db": "1.48.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.5.1.tgz", + "integrity": "sha512-wEpr0XooH6rw/Mlf+9KTJoMBLT3HujzdTrmohPjAzF47N4Q6yAeczQLpRD/WxvAtXvskcXbily7TAdCfi2M4Dg==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz", + "integrity": "sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==", + "dev": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "needle": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, + "node-gyp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-releases": { + "version": "1.1.73", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", + "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", + "optional": true + }, + "npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.2.tgz", + "integrity": "sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz", + "integrity": "sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz", + "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==", + "dev": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "npm-registry-fetch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-10.1.2.tgz", + "integrity": "sha512-KsM/TdPmntqgBFlfsbkOLkkE9ovZo7VpVcd+/eTdYszCrgy5zFl5JzWm+OxavFaEWlbkirpkou+ZYI00RmOBFA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0", + "make-fetch-happen": "^8.0.9", + "minipass": "^3.1.3", + "minipass-fetch": "^1.3.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.0.2.tgz", + "integrity": "sha512-NV5QmWJrTaNBLHABJyrb+nd5dXI5zfea/suWawBhkHzAbVhLLiJdrqMgxMypGK9Eznp2Ltoh7SAVkQ3XAucX7Q==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + } + } + }, + "ora": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz", + "integrity": "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pacote": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.3.2.tgz", + "integrity": "sha512-lMO7V9aMhyE5gfaSFxKfW3OTdXuFBNQJfuNuet3NPzWWhOYIW90t85vHcHLDjdhgmfAdAHyh9q1HAap96ea0XA==", + "dev": true, + "requires": { + "@npmcli/git": "^2.0.1", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^1.8.2", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^2.1.4", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^10.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "dev": true, + "requires": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz", + "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + } + }, + "postcss-attribute-case-insensitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", + "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz", + "integrity": "sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-color-functional-notation": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", + "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-color-gray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", + "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-color-hex-alpha": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", + "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "dev": true, + "requires": { + "postcss": "^7.0.14", + "postcss-values-parser": "^2.0.1" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-color-mod-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", + "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-color-rebeccapurple": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", + "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-colormin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz", + "integrity": "sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==", + "dev": true, + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-convert-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz", + "integrity": "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-custom-media": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", + "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-custom-properties": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", + "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", + "dev": true, + "requires": { + "postcss": "^7.0.17", + "postcss-values-parser": "^2.0.1" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-custom-selectors": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", + "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-dir-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", + "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-discard-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "dev": true + }, + "postcss-discard-duplicates": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "dev": true + }, + "postcss-discard-empty": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "dev": true + }, + "postcss-discard-overridden": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "dev": true + }, + "postcss-double-position-gradients": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", + "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", + "dev": true, + "requires": { + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-env-function": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", + "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-focus-visible": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", + "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-focus-within": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", + "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-font-variant": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", + "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-gap-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", + "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-image-set-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", + "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-import": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.1.tgz", + "integrity": "sha512-Xn2+z++vWObbEPhiiKO1a78JiyhqipyrXHBb3AHpv0ks7Cdg+GxQQJ24ODNMTanldf7197gSP3axppO9yaG0lA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz", + "integrity": "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-lab-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", + "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "dev": true, + "requires": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-loader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-5.2.0.tgz", + "integrity": "sha512-uSuCkENFeUaOYsKrXm0eNNgVIxc71z8RcckLMbVw473rGojFnrUeqEz6zBgXsH2q1EIzXnO/4pEz9RhALjlITA==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.4", + "semver": "^7.3.4" + } + }, + "postcss-logical": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", + "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-media-minmax": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", + "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-merge-longhand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz", + "integrity": "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==", + "dev": true, + "requires": { + "css-color-names": "^1.0.1", + "postcss-value-parser": "^4.1.0", + "stylehacks": "^5.0.1" + } + }, + "postcss-merge-rules": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", + "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", + "dev": true, + "requires": { + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^2.0.1", + "postcss-selector-parser": "^6.0.5", + "vendors": "^1.0.3" + } + }, + "postcss-minify-font-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz", + "integrity": "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-minify-gradients": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz", + "integrity": "sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "is-color-stop": "^1.1.0", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-minify-params": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", + "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "browserslist": "^4.16.0", + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0", + "uniqs": "^2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz", + "integrity": "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nesting": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", + "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "dev": true + }, + "postcss-normalize-display-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-positions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz", + "integrity": "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz", + "integrity": "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-string": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz", + "integrity": "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz", + "integrity": "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-unicode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz", + "integrity": "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-url": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", + "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", + "dev": true, + "requires": { + "is-absolute-url": "^3.0.3", + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-normalize-whitespace": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz", + "integrity": "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-ordered-values": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz", + "integrity": "sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-overflow-shorthand": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", + "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-page-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", + "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-place": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", + "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-preset-env": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", + "integrity": "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==", + "dev": true, + "requires": { + "autoprefixer": "^9.6.1", + "browserslist": "^4.6.4", + "caniuse-lite": "^1.0.30000981", + "css-blank-pseudo": "^0.1.4", + "css-has-pseudo": "^0.10.0", + "css-prefers-color-scheme": "^3.1.1", + "cssdb": "^4.4.0", + "postcss": "^7.0.17", + "postcss-attribute-case-insensitive": "^4.0.1", + "postcss-color-functional-notation": "^2.0.1", + "postcss-color-gray": "^5.0.0", + "postcss-color-hex-alpha": "^5.0.3", + "postcss-color-mod-function": "^3.0.3", + "postcss-color-rebeccapurple": "^4.0.1", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^8.0.11", + "postcss-custom-selectors": "^5.1.2", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-double-position-gradients": "^1.0.0", + "postcss-env-function": "^2.0.2", + "postcss-focus-visible": "^4.0.0", + "postcss-focus-within": "^3.0.0", + "postcss-font-variant": "^4.0.0", + "postcss-gap-properties": "^2.0.0", + "postcss-image-set-function": "^3.0.1", + "postcss-initial": "^3.0.0", + "postcss-lab-function": "^2.0.1", + "postcss-logical": "^3.0.0", + "postcss-media-minmax": "^4.0.0", + "postcss-nesting": "^7.0.0", + "postcss-overflow-shorthand": "^2.0.0", + "postcss-page-break": "^2.0.0", + "postcss-place": "^4.0.1", + "postcss-pseudo-class-any-link": "^6.0.0", + "postcss-replace-overflow-wrap": "^3.0.0", + "postcss-selector-matches": "^4.0.0", + "postcss-selector-not": "^4.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-pseudo-class-any-link": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", + "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "dev": true, + "requires": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "dependencies": { + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-reduce-initial": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", + "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz", + "integrity": "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==", + "dev": true, + "requires": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-replace-overflow-wrap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", + "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", + "dev": true, + "requires": { + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-not": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", + "integrity": "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.2.tgz", + "integrity": "sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.1.0", + "svgo": "^2.3.0" + } + }, + "postcss-unique-selectors": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", + "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "postcss-values-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", + "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + } + } + }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-package-json-fast": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.2.tgz", + "integrity": "sha512-5fyFUyO9B799foVk4n6ylcoAktG/FbE3jwRKxvwaeSrIunaoMc0u81dzXxjeAFKOce7O5KncdfwpGvvs6r5PsQ==", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "regjsparser": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", + "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.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" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "resolve-url-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", + "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^7.0.35", + "source-map": "0.6.1" + }, + "dependencies": { + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass": { + "version": "1.32.12", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.12.tgz", + "integrity": "sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0" + } + }, + "sass-loader": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-11.0.1.tgz", + "integrity": "sha512-Vp1LcP4slTsTNLEiDkTcm8zGN/XYYrZz2BZybQbliWA8eXveqA/AxsEjllQTpJbg2MzCsx/qNO48sHdZtOaxTw==", + "dev": true, + "requires": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", + "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socket.io": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", + "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", + "dev": true, + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.1", + "engine.io": "~4.1.0", + "socket.io-adapter": "~2.1.0", + "socket.io-parser": "~4.0.3" + } + }, + "socket.io-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", + "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==", + "dev": true + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dev": true, + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, + "sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + } + }, + "sockjs-client": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.1.tgz", + "integrity": "sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "eventsource": "^1.0.7", + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.5.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + }, + "source-map-loader": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-2.0.1.tgz", + "integrity": "sha512-UzOTTQhoNPeTNzOxwFw220RSRzdGSyH4lpNyWjR7Qm34P4/N0W669YSUFdH07+YNeN75h765XLHmNsF/bm97RQ==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "source-map-js": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "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" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "streamroller": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", + "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", + "dev": true, + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "stylehacks": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", + "integrity": "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "postcss-selector-parser": "^6.0.4" + } + }, + "stylus": { + "version": "0.54.8", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", + "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", + "dev": true, + "requires": { + "css-parse": "~2.0.0", + "debug": "~3.1.0", + "glob": "^7.1.6", + "mkdirp": "~1.0.4", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "semver": "^6.3.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "stylus-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-5.0.0.tgz", + "integrity": "sha512-1OaGgixTgC8IAaMCodZXg7XYsfP1qU0UzTHDyPaWACUh34j9geJL4iA583tFJDOtfNUOfDLaBpUywc5MicQ1aA==", + "dev": true, + "requires": { + "fast-glob": "^3.2.5", + "klona": "^2.0.4", + "normalize-path": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.3.1.tgz", + "integrity": "sha512-riDDIQgXpEnn0BEl9Gvhh1LNLIyiusSpt64IR8upJu7MwxnzetmF/Y57pXQD2NMX2lVyMRzXt5f2M5rO4wG7Dw==", + "dev": true, + "requires": { + "@trysound/sax": "0.1.1", + "chalk": "^4.1.0", + "commander": "^7.1.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.2", + "csso": "^4.2.0", + "stable": "^0.1.8" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true + }, + "tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "dev": true + }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.2.tgz", + "integrity": "sha512-6QhDaAiVHIQr5Ab3XUWZyDmrIPCHMiqJVljMF91YKyqwKkL5QHnYMkrMBy96v9Z7ev1hGhSEw1HQZc2p/s5Z8Q==", + "dev": true, + "requires": { + "jest-worker": "^26.6.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.7.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.28", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", + "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", + "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", + "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webpack": { + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.39.1.tgz", + "integrity": "sha512-ulOvoNCh2PvTUa+zbpRuEb1VPeQnhxpnHleMPVVCq3QqnaFogjsLyps+o42OviQFoaGtTQYrUqDXu1QNkvUPzw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.47", + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/wasm-edit": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "acorn": "^8.2.1", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.8.0", + "es-module-lexer": "^0.4.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.1", + "watchpack": "^2.2.0", + "webpack-sources": "^2.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "webpack-sources": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz", + "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.1.0.tgz", + "integrity": "sha512-mpa/FY+DiBu5+r5JUIyTCYWRfkWgyA3/OOE9lwfzV9S70A4vJYLsVRKj5rMFEsezBroy2FmPyQ8oBRVW8QmK1A==", + "dev": true, + "requires": { + "colorette": "^1.2.1", + "mem": "^8.0.0", + "memfs": "^3.2.0", + "mime-types": "^2.1.28", + "range-parser": "^1.2.1", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-dev-server": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", + "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "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-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", + "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-subresource-integrity": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz", + "integrity": "sha512-GBWYBoyalbo5YClwWop9qe6Zclp8CIXYGIz12OPclJhIrSplDxs1Ls1JDMH8xBPPrg1T6ISaTW9Y6zOrwEiAzw==", + "dev": true, + "requires": { + "webpack-sources": "^1.3.0" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zone.js": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.4.tgz", + "integrity": "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==", + "requires": { + "tslib": "^2.0.0" + } + } + } +} diff --git a/site-portal/frontend/package.json b/site-portal/frontend/package.json new file mode 100644 index 00000000..0fd5ca90 --- /dev/null +++ b/site-portal/frontend/package.json @@ -0,0 +1,56 @@ +{ + "name": "Site-portal", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve --host 0.0.0.0 --port 4001", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "~12.0.5", + "@angular/cdk": "^12.1.1", + "@angular/common": "~12.0.5", + "@angular/compiler": "~12.0.5", + "@angular/core": "~12.0.5", + "@angular/forms": "~12.0.5", + "@angular/localize": "~12.0.5", + "@angular/platform-browser": "~12.0.5", + "@angular/platform-browser-dynamic": "~12.0.5", + "@angular/router": "~12.0.5", + "@cds/core": "^5.4.1", + "@clr/angular": "^5.4.1", + "@clr/ui": "^5.4.1", + "@ng-bootstrap/ng-bootstrap": "^10.0.0", + "@ngx-translate/core": "^13.0.0", + "@ngx-translate/http-loader": "^6.0.0", + "@webcomponents/webcomponentsjs": "^2.5.0", + "dagre-d3": "^0.6.4", + "file-saver": "^2.0.5", + "jquery": "^3.6.0", + "jwt-decode": "^3.1.2", + "moment": "^2.29.1", + "rxjs": "~6.6.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~12.0.5", + "@angular/cli": "~12.0.5", + "@angular/compiler-cli": "~12.0.5", + "@types/d3": "^7.0.0", + "@types/dagre-d3": "^0.6.3", + "@types/file-saver": "^2.0.3", + "@types/jquery": "^3.5.9", + "@types/node": "^12.11.1", + "@types/jasmine": "~3.6.0", + "jasmine-core": "~3.7.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", + "typescript": "~4.2.3" + } +} diff --git a/site-portal/frontend/proxy.conf.json b/site-portal/frontend/proxy.conf.json new file mode 100644 index 00000000..2d83fd67 --- /dev/null +++ b/site-portal/frontend/proxy.conf.json @@ -0,0 +1,8 @@ + { + "/api/v1": { + "target": "https://localhost:8443", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + } + } diff --git a/site-portal/frontend/src/app/app-routing.module.ts b/site-portal/frontend/src/app/app-routing.module.ts new file mode 100644 index 00000000..f95e6b1e --- /dev/null +++ b/site-portal/frontend/src/app/app-routing.module.ts @@ -0,0 +1,120 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DataDetailsComponent } from './view/data-details/data-details.component'; +import { DataMgComponent } from './view/data-mg/data-mg.component'; +import { ModelDetailComponent } from './view/model-detail/model-detail.component'; +import { ModelMgComponent } from './view/model-mg/model-mg.component'; +import { ProjectMgComponent } from './view/project-mg/project-mg.component'; +import { SiteConfigComponent } from './view/site-config/site-config.component'; +import { LogInComponent } from './view/log-in/log-in.component'; +import { UserMgComponent } from './view/user-mg/user-mg.component'; +import { HomeComponent } from './view/home/home.component' +import { SelectivePreloadingStrategy } from 'src/utils/selective-preloading-strategy'; +const routes: Routes = [ + { + path: 'login', + component: LogInComponent + }, + { + path: '', + component: HomeComponent, + data: { + preload: true + }, + children: [ + { + path: '', + redirectTo: 'project-management', + pathMatch: 'full' + }, + { + path: 'model-management', + data: { + preload: true + }, + component: ModelMgComponent + }, + { + path: 'site-configuration', + data: { + preload: true + }, + component: SiteConfigComponent, + }, + { + path: 'user-management', + data: { + preload: true + }, + component: UserMgComponent + }, + { + path: 'site-configuration', + data: { + preload: true + }, + component: SiteConfigComponent + }, + { + path: 'model-detail/:id', + data: { + preload: true + }, + component: ModelDetailComponent + }, + { + path: 'data-management', + data: { + preload: true + }, + component: DataMgComponent + }, + { + path: 'data-detail/:data_id', + data: { + preload: true + }, + component: DataDetailsComponent + }, + { + path: 'project-management', + data: { + preload: true + }, + component: ProjectMgComponent + }, + { + path: 'project-management/project-detail/:id', + loadChildren: () => import('./view/project-details/project-detail.module').then(m => m.ProjectDetailModule) + }, + { + path: 'project-management/project-detail/:projid/job/job-detail/:jobid', + loadChildren: () => import('./view/job-detail/job-detail.module').then(m => m.JobDetailModule) + }, + { + path: 'project-management/project-detail/:id/job/new-job', + loadChildren: () => import('./view/job-new/job-new.module').then(m => m.JobNewModule) + } + ] + }, + +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { + preloadingStrategy: SelectivePreloadingStrategy + })], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/site-portal/frontend/src/app/app.component.css b/site-portal/frontend/src/app/app.component.css new file mode 100644 index 00000000..e69de29b diff --git a/site-portal/frontend/src/app/app.component.html b/site-portal/frontend/src/app/app.component.html new file mode 100644 index 00000000..6a164d69 --- /dev/null +++ b/site-portal/frontend/src/app/app.component.html @@ -0,0 +1,2 @@ + + diff --git a/site-portal/frontend/src/app/app.component.spec.ts b/site-portal/frontend/src/app/app.component.spec.ts new file mode 100644 index 00000000..db509b98 --- /dev/null +++ b/site-portal/frontend/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'Site-portal'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('Site-portal'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('Site-portal app is running!'); + }); +}); diff --git a/site-portal/frontend/src/app/app.component.ts b/site-portal/frontend/src/app/app.component.ts new file mode 100644 index 00000000..3cdd2e6e --- /dev/null +++ b/site-portal/frontend/src/app/app.component.ts @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { AppService } from './app.service' +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'Site-portal'; + constructor (private i18: AppService) { + const lang = localStorage.getItem('Site-Portal-Language') + if (lang) { + this.i18.changeLanguage(lang) + } else { + if (window.navigator.language === 'zh-CN') { + this.i18.changeLanguage('zh_CN') + } else { + this.i18.changeLanguage('en') + } + } + } +} diff --git a/site-portal/frontend/src/app/app.module.ts b/site-portal/frontend/src/app/app.module.ts new file mode 100644 index 00000000..20f5300d --- /dev/null +++ b/site-portal/frontend/src/app/app.module.ts @@ -0,0 +1,86 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { ClarityModule } from '@clr/angular'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HeaderComponent } from './components/header/header.component'; +import { SidenavComponent } from './components/sidenav/sidenav.component'; +import { ModelMgComponent } from './view/model-mg/model-mg.component'; +import { SiteConfigComponent } from './view/site-config/site-config.component'; +import { UserMgComponent } from './view/user-mg/user-mg.component'; +import { ModelDetailComponent } from './view/model-detail/model-detail.component'; +import { LogInComponent } from './view/log-in/log-in.component'; +import { DataMgComponent } from './view/data-mg/data-mg.component'; +import { DataDetailsComponent } from './view/data-details/data-details.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { ProjectMgComponent } from './view/project-mg/project-mg.component'; +import { AuthInterceptor } from '../utils/auth-interceptor'; +import { MessageModule } from './components/message/message.module'; +import { AppService, createTranslateLoader } from './app.service' +import { TranslateModule, TranslateLoader } from '@ngx-translate/core' +import { FilterComponent } from './components/filter/filter.component'; +import { HomeComponent } from './view/home/home.component' +import { SelectivePreloadingStrategy } from '../utils/selective-preloading-strategy' +import { ShardModule } from 'src/app/shared/shard/shard.module'; +import { UploadFileComponent } from './components/upload-file/upload-file.component'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + SidenavComponent, + ModelMgComponent, + SiteConfigComponent, + UserMgComponent, + ModelDetailComponent, + LogInComponent, + DataMgComponent, + DataDetailsComponent, + ProjectMgComponent, + FilterComponent, + HomeComponent, + UploadFileComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + ClarityModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + HttpClientModule, + MessageModule, + ShardModule, + TranslateModule.forRoot({// 配置i8n + defaultLanguage: 'en', + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient] + } + }), + NgbModule + ], + providers: [ + AppService, + SelectivePreloadingStrategy, + [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }] + ], + bootstrap: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class AppModule { } diff --git a/site-portal/frontend/src/app/app.service.spec.ts b/site-portal/frontend/src/app/app.service.spec.ts new file mode 100644 index 00000000..3a3b746f --- /dev/null +++ b/site-portal/frontend/src/app/app.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AppService } from './app.service'; + +describe('AppService', () => { + let service: AppService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AppService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/app.service.ts b/site-portal/frontend/src/app/app.service.ts new file mode 100644 index 00000000..2086f752 --- /dev/null +++ b/site-portal/frontend/src/app/app.service.ts @@ -0,0 +1,35 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core' +import { HttpClient } from '@angular/common/http'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +@Injectable() +export class AppService { + public lang = 'en' + public langList = ['en', 'zh_CN'] + constructor(public translate: TranslateService) { + this.translate.addLangs(this.langList) + this.translate.setDefaultLang(this.lang) + this.translate.use(this.lang) + } + changeLanguage(lang: string) { + this.lang = lang + localStorage.setItem('Site-Portal-Language', lang) + this.translate.setDefaultLang(this.lang) + this.translate.use(this.lang) + } + +} +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, '../assets/i18n/', '.json') +} diff --git a/site-portal/frontend/src/app/components/components-loader.ts b/site-portal/frontend/src/app/components/components-loader.ts new file mode 100644 index 00000000..4f0d107a --- /dev/null +++ b/site-portal/frontend/src/app/components/components-loader.ts @@ -0,0 +1,54 @@ +import { + ComponentFactoryResolver, + ComponentRef, + Type, + Injector, + Provider, + ElementRef, + ComponentFactory, + Injectable +} from '@angular/core' +export class ComponentLoader { + constructor(private _cfr: ComponentFactoryResolver, + private _injector: Injector) { + } + private _componentFactory: ComponentFactory | any + attch(componentType: Type): ComponentLoader { + this._componentFactory = this._cfr.resolveComponentFactory(componentType) + return this + } + private _parent: Element | any + to(parent: string | ElementRef): ComponentLoader { + if (parent instanceof ElementRef) { + this._parent = parent.nativeElement + } else { + this._parent = document.querySelector(parent) + } + return this + } + private _providers: Provider[] = [] + provider(provider: Provider) { + this._providers.push(provider) + } + create(opts: {}): ComponentRef { + const injector = Injector.create({ + providers: this._providers as any[], + parent: this._injector, + name: '$msg' + }) + const componentRef = this._componentFactory.create(injector) + Object.assign(componentRef.instance, opts) + if (this._parent) { + this._parent.appendChild(componentRef.location.nativeElement) + } + componentRef.changeDetectorRef.markForCheck(); + componentRef.changeDetectorRef.detectChanges(); + return componentRef; + } + remove(ref: ComponentRef | any) { + if (this._parent) { + this._parent.removeChild(ref.location.nativeElement) + } + ref = null; + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/filter/filter.component.css b/site-portal/frontend/src/app/components/filter/filter.component.css new file mode 100644 index 00000000..2272df00 --- /dev/null +++ b/site-portal/frontend/src/app/components/filter/filter.component.css @@ -0,0 +1,8 @@ + +.searchinput { + width: 150px; +} + +.clr-form.position.searchform .clr-row /deep/ .clr-form-control { + margin-top: 0px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/filter/filter.component.html b/site-portal/frontend/src/app/components/filter/filter.component.html new file mode 100644 index 00000000..50923925 --- /dev/null +++ b/site-portal/frontend/src/app/components/filter/filter.component.html @@ -0,0 +1,16 @@ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/components/filter/filter.component.spec.ts b/site-portal/frontend/src/app/components/filter/filter.component.spec.ts new file mode 100644 index 00000000..f43cc0d6 --- /dev/null +++ b/site-portal/frontend/src/app/components/filter/filter.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterComponent } from './filter.component'; + +describe('FilterComponent', () => { + let component: FilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/filter/filter.component.ts b/site-portal/frontend/src/app/components/filter/filter.component.ts new file mode 100644 index 00000000..77a21077 --- /dev/null +++ b/site-portal/frontend/src/app/components/filter/filter.component.ts @@ -0,0 +1,71 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//This component is for list filter +import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +@Component({ + selector: 'app-filter', + templateUrl: './filter.component.html', + styleUrls: ['./filter.component.css'] +}) +export class FilterComponent implements OnInit, OnChanges { + constructor() { + } + ngOnChanges(changes: SimpleChanges): void { + this._datalist = this.dataList.map(el => { + el[this.searchKey] = el[this.searchKey].toLowerCase() + if (this.searchKey2 !== '') { + el[this.searchKey2] = el[this.searchKey2].toLowerCase() + } + return el + }) + } + @Input() left = 'auto' + @Input() right = '0' + @Input() top = 'auto' + @Input() bottom = 'auto' + @Input() width = 300 + @Input() dataList: any[] = [] // data + @Input() searchKey: string = '' + @Input() searchKey2: string = '' + @Input() placeholder: string = '' + @Output() filterDataList = new EventEmitter() + fixDataView() { + const data = { + searchValue: this.filterSearchValue, + eligibleList: this.eligibleList + } + this.filterDataList.emit(data) + } + public filterSearchValue: string = '' + public eligibleList: any[] = [] + private _datalist: any[] = [] + private _filterSearchValue = '' + private storageDataList: any[] = [] + ngOnInit(): void { + this.storageDataList = this.dataList + } + + inputHandle() { + this.eligibleList = [] + this._filterSearchValue = this.filterSearchValue.toLowerCase() + if (!this.filterSearchValue.trim()) { + this.fixDataView() + return + } + this._datalist.forEach((el: any, index: number) => { + if (el[this.searchKey].indexOf(this._filterSearchValue) !== -1 || (this.searchKey2 !== '' && el[this.searchKey2].indexOf(this._filterSearchValue) !== -1)) { + this.eligibleList.push(this.dataList[index]) + } + }); + this.fixDataView() + } +} diff --git a/site-portal/frontend/src/app/components/header/header.component.css b/site-portal/frontend/src/app/components/header/header.component.css new file mode 100644 index 00000000..1332a610 --- /dev/null +++ b/site-portal/frontend/src/app/components/header/header.component.css @@ -0,0 +1,45 @@ +.dropdown{ + right: 25px; + top: 50%; + transform: translateY(-50%); + z-index: 999; +} +.header { +justify-content: space-between; +} +.login-btn-wrap { + position: relative; +} +.dropdown-menu { + min-width:2px; +} + +.dropdown-toggle.btn.btn-link { + color: #fff; +} +.usericon { + margin: 4px; + margin-bottom: 6px; +} +clr-password-container input { + width: 250px +} +.alert { + padding-left: 6px; + padding-top: 6px; + padding-bottom: 6px; + padding-right: 6px; + margin: 6px; +} +.iconImage { + width: 36px; + height: 36px; + margin-right: 12px; +} +.angleicon { + margin-right: -4px; + margin-top: -3px; +} +.language-dropdown { + margin-right: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/header/header.component.html b/site-portal/frontend/src/app/components/header/header.component.html new file mode 100644 index 00000000..9401adbc --- /dev/null +++ b/site-portal/frontend/src/app/components/header/header.component.html @@ -0,0 +1,100 @@ +
+ + +
+ + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/header/header.component.spec.ts b/site-portal/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 00000000..381e8e80 --- /dev/null +++ b/site-portal/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderComponent } from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HeaderComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/header/header.component.ts b/site-portal/frontend/src/app/components/header/header.component.ts new file mode 100644 index 00000000..35ffd585 --- /dev/null +++ b/site-portal/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,148 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import '@cds/core/icon/register.js'; +import { checkCircleIcon, ClarityIcons, exclamationCircleIcon, userIcon, vmBugIcon, worldIcon } from '@cds/core/icon'; +import { AuthService } from 'src/app/service/auth.service'; +import { Router } from '@angular/router'; +import { SiteService } from 'src/app/service/site.service'; +import { uncompile } from '../../../utils/compile' +import { AppService } from 'src/app/app.service' +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ConfirmedValidator } from '../../../config/validators' +import { UserMgService } from 'src/app/service/user-mg.service'; +import { MessageService } from 'src/app/components/message/message.service'; + +ClarityIcons.addIcons(userIcon, checkCircleIcon, checkCircleIcon, exclamationCircleIcon, worldIcon, vmBugIcon); + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.css'] +}) + +export class HeaderComponent implements OnInit { + open: boolean = false; + returnUrl: string = "/login"; + isLoggedIn = true; + isLoginFailed = false; + errorMessage = ''; + isLogoutFailed = false; + isLogOutSubmit = false; + form: FormGroup; + constructor(private authService: AuthService, private router: Router, private siteService: SiteService, public i18: AppService, private fb: FormBuilder, private userService: UserMgService, private msg: MessageService) { + //change password form + this.form = this.fb.group({ + curPassword: [''], + newPassword: ['', [Validators.required]], + confirmPassword: ['', [Validators.required]] + }, { + validator: ConfirmedValidator('newPassword', 'confirmPassword') + }) + } + + ngOnInit(): void { + // get stored username + const username = sessionStorage.getItem('username') + if (username) { + this.username = uncompile(username) + } else { + // get current user + this.siteService.getCurrentUser().subscribe( + data => { + if (data.data) { + this.username = data.data + } + }, + err => { + this.router.navigate(['/login']) + } + ) + } + } + + username: string = ''; + condition: boolean = false; + langFlag: boolean = false + //Trigger user dropdown menu + toggleDropdown() { + this.condition = !this.condition; + if (this.condition) this.langFlag = false; + } + + //Trigger language dropdown menu + languageDropdown() { + this.langFlag = !this.langFlag; + if (this.langFlag) this.condition = false; + } + + //logout button + logout(): void { + this.isLogOutSubmit = true; + this.authService.logout() + .subscribe( + data => { + this.isLoggedIn = false; + sessionStorage.removeItem('username'); + sessionStorage.removeItem('userId') + this.router.navigate([this.returnUrl]); + }, + err => { + this.errorMessage = err.error.message; + this.isLogoutFailed = true; + } + ); + } + + curPassword: any = ''; + newPassword: any = ''; + confirmPassword: any = ''; + openModal: boolean = false; + //reset 'change password' modal when close + resetModal() { + this.form.reset(); + this.openModal = false; + this.isChangePwdSubmit = false; + this.isChangePwdFailed = false; + this.isChangePwdSuccessed = false; + } + + isChangePwdSubmit: boolean = false; + isChangePwdFailed: boolean = false; + isChangePwdSuccessed: boolean = false; + //submit 'change password' request + changePassword() { + var userId = sessionStorage.getItem('userId'); + this.isChangePwdSubmit = true; + if (!this.form.valid) { + this.isChangePwdFailed = true; + this.errorMessage = "Invaild input." + return; + } + this.userService.changePassword(this.curPassword, this.newPassword, userId) + .subscribe(data => { + this.isChangePwdFailed = false; + this.isChangePwdSuccessed = true; + setTimeout(() => { + this.logout(); + }, 3000) + }, + err => { + this.isChangePwdFailed = true; + if (err.error.message === "crypto/bcrypt: hashedPassword is not the hash of the given password") { + this.errorMessage = "The input of current password is incorrect. " + } else { + this.errorMessage = err.error.message; + } + }); + } +} diff --git a/site-portal/frontend/src/app/components/high-json/high-json.component.css b/site-portal/frontend/src/app/components/high-json/high-json.component.css new file mode 100644 index 00000000..8057d1d4 --- /dev/null +++ b/site-portal/frontend/src/app/components/high-json/high-json.component.css @@ -0,0 +1,73 @@ +.json-document { + padding: 1em 2em; +} +/* Syntax highlighting for JSON objects */ +ul.json-dict, ol.json-array { + list-style-type: none; + margin: 0 0 0 1px; + border-left: 1px dotted #ccc; + padding-left: 2em; +} +.json-string { + color: #0B7500; +} +.json-literal { + color: #1A01CC; + font-weight: bold; +} + +/* Toggle button */ +a.json-toggle { + position: relative; + color: inherit; + text-decoration: none; +} +a.json-toggle:focus { + outline: none; +} +a.json-toggle:before { + font-size: 1.1em; + color: #c0c0c0; + content: "\25BC"; /* down arrow */ + position: absolute; + display: inline-block; + width: 1em; + text-align: center; + line-height: 1em; + left: -1.2em; +} +a.json-toggle:hover:before { + color: #aaa; +} +a.json-toggle.collapsed:before { + /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */ + transform: rotate(-90deg); +} + +/* Collapsable placeholder links */ +a.json-placeholder { + color: #aaa; + padding: 0 1em; + text-decoration: none; +} +a.json-placeholder:hover { + text-decoration: underline; +} +/* [jquery.json-viewer.css] END */ + +body { + font-family: sans-serif; +} +p.options label { + margin-right: 10px; +} +p.options input[type=checkbox] { + vertical-align: middle; +} +textarea#json-input { + width: 100%; + height: 200px; +} +pre#json-renderer { + border: 1px solid #aaa; +} diff --git a/site-portal/frontend/src/app/components/high-json/high-json.component.html b/site-portal/frontend/src/app/components/high-json/high-json.component.html new file mode 100644 index 00000000..6adde119 --- /dev/null +++ b/site-portal/frontend/src/app/components/high-json/high-json.component.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/site-portal/frontend/src/app/components/high-json/high-json.component.spec.ts b/site-portal/frontend/src/app/components/high-json/high-json.component.spec.ts new file mode 100644 index 00000000..5e2d0f5f --- /dev/null +++ b/site-portal/frontend/src/app/components/high-json/high-json.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HighJsonComponent } from './high-json.component'; + +describe('HighJsonComponent', () => { + let component: HighJsonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HighJsonComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HighJsonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/high-json/high-json.component.ts b/site-portal/frontend/src/app/components/high-json/high-json.component.ts new file mode 100644 index 00000000..68166c87 --- /dev/null +++ b/site-portal/frontend/src/app/components/high-json/high-json.component.ts @@ -0,0 +1,137 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ViewEncapsulation, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; +import { isCollapsable, json2html, valueSplice } from 'src/utils/high-json' +import * as $ from 'jquery' +@Component({ + selector: 'app-high-json', + templateUrl: './high-json.component.html', + styleUrls: ['./high-json.component.css'], + encapsulation: ViewEncapsulation.None +}) + +//This component is for highlighting json content +export class HighJsonComponent implements OnInit, OnChanges { + private _$: any = {} + @Input() json: string = "" + @Input() id = "" + @Output() callback = new EventEmitter() + callbackFun(data: string) { + this.callback.emit(data) + } + @Output() callback2 = new EventEmitter() + callbackFun2(data: string) { + this.callback.emit(data) + } + jsonObj: any = {} + isCollapsable!: Function + valueSplice!: Function + constructor() { + this._$ = $ + this.valueSplice = valueSplice + this._$.fn.jsonViewer = function (json: string, options: any) { + options = Object.assign({}, { + collapsed: false, + rootCollapsable: true, + withQuotes: false, + withLinks: true + }, options); + return this.each(() => { + // Transform to HTML + var html = json2html(json, options); + if (options.rootCollapsable && isCollapsable(json)) { + html = '' + html; + } + // Insert HTML in target DOM element + $(this[0]).html(html); + $(this[0]).addClass('json-document'); + // Bind click on toggle buttons + $(this[0]).off('click'); + $(this[0]).on('click', 'a.json-toggle', function () { + var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); + target.toggle(); + if (target.is(':visible')) { + target.siblings('.json-placeholder').remove(); + } else { + var count = target.children('li').length; + var placeholder = count + (count > 1 ? ' items' : ' item'); + target.after('' + placeholder + ''); + } + return false; + }); + // Simulate click on toggle button when placeholder is clicked + $(this[0]).on('click', 'a.json-placeholder', function () { + $(this[0]).siblings('a.json-toggle').click(); + return false; + }); + if (options.collapsed == true) { + // Trigger click to collapse all nodes + $(this[0]).find('a.json-toggle').click(); + } + }); + } + + } + ngOnChanges(changes: SimpleChanges): void { + this.updateJsonAttr(this.id) + } + ngOnInit(): void { + this.updateJsonAttr(this.id) + } + + updateJsonAttr(id: string) { + this.jsonObj = JSON.parse(this.json) + const obj = JSON.parse(this.json) + this._$('#' + id).jsonViewer(obj, {}) + const _this = this + $('#' + id + ' li').click(function (event: any) { + if (event.target.classList[0] === 'json-string' || event.target.classList[0] === 'json-literal') { + event.stopPropagation() + const input = document.createElement('input') + input.className = 'update' + let str + let that: any = {} + if (event.target.innerText.indexOf('"') !== -1) { + str = event.target.innerText.replace(/\"/g, '') + input.value = str + } else { + input.value = event.target.innerText + } + let liStr:any = this.innerText.split(':') + that = event.target + this.appendChild(input) + input.focus() + $('input.update').blur(function (event: any) { + const valueType = +event.target.value + if (isNaN(valueType)) { + if (event.target.value === 'true' || event.target.value === 'false') { + that.innerText = event.target.value + liStr[1] = event.target.value + } else { + that.innerText = '"' + event.target.value + '"' + liStr[1] = event.target.value + } + } else { + that.innerText = valueType + liStr[1] = valueType + } + // liStr[1] = event.target.value + _this.valueSplice(liStr, obj) + + _this.jsonObj = obj + event.target.parentElement.removeChild(event.target) + }) + + } + }) + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/loaderFactory.ts b/site-portal/frontend/src/app/components/loaderFactory.ts new file mode 100644 index 00000000..6e7ba32c --- /dev/null +++ b/site-portal/frontend/src/app/components/loaderFactory.ts @@ -0,0 +1,18 @@ +import { + ComponentFactoryResolver, + Injector, + Injectable +} from '@angular/core'; +import { ComponentLoader } from './components-loader'; + +@Injectable() +export class ComponentLoaderFactory { + constructor(private _injector: Injector, + private _cfr: ComponentFactoryResolver) { + + } + + create(): ComponentLoader { + return new ComponentLoader(this._cfr, this._injector); + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/message/message.component.css b/site-portal/frontend/src/app/components/message/message.component.css new file mode 100644 index 00000000..5f2054f9 --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.component.css @@ -0,0 +1,102 @@ +.upc-message { + position: fixed; + z-index: 1999; + width: 100%; + top: 36px; + left: 0; + pointer-events: none; + padding: 8px; + text-align: center; + } + .hide { + display: none; + } + .upc-message-content { + position: relative; + padding: 8px 16px; + padding-left: 40px; + min-width: 300px; + max-width: 800px; + text-align: left; + font-size: 0.65rem; + color: #333; + line-height: 24px; + -ms-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 2px 8px #000000; + -ms-box-shadow: 0 2px 8px #000000; + box-shadow: 0 2px 8px #000000; + box-shadow: 0 2px 8px rgba(0,0,0,.2); + background: #fff; + display: inline-block; + pointer-events: all; + } + .upc-message-content .content-msg { + margin: 0px; + } + .upc-message-content .content-icon { + position: absolute; + left: 16px; + } + .special { + height: 24px; + overflow: hidden; + } + .upc-message-success{ + background: #DFF0D0; + border: 1px solid #306B00; + } + .upc-message-warning { + background: #FDF4C7; + border: 1px solid #AD7600; + } + .upc-message-error{ + background: #FCDDD7; + border: 1px solid #991700; + color: #666; + } + .upc-message-info{ + background: #E3F5FC; + border: 1px solid #00567a; + color: #666; + } + .upc-message-hide{ + display: block; + } +.close { + position: absolute; + top: -10px; + right: -10px; + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #00567a; + text-align: center; + margin: 0px; + line-height: 18px; + color: #00567a; + font-weight: bold; + background: #E3F5FC; + opacity: 1; +} +.close-success { + background: #DFF0D0; + border: 2px solid #306B00; + color: #306B00; +} +.close-warning { + background: #FDF4C7; + border: 2px solid #AD7600; + color: #AD7600; +} +.close-error { + background: #FCDDD7; + border: 2px solid #991700; + color: #991700; +} +.close-info { + background: #E3F5FC; + border: 2px solid #00567a; + color: #00567a; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/message/message.component.html b/site-portal/frontend/src/app/components/message/message.component.html new file mode 100644 index 00000000..395447a0 --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.component.html @@ -0,0 +1,17 @@ +
+
+

X

+ + + + + + +
+
{{messageContent | translate}}
+
用户信息已失效,请重新登录
+
Invaild authentication. Please log in.
+
+
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/components/message/message.component.spec.ts b/site-portal/frontend/src/app/components/message/message.component.spec.ts new file mode 100644 index 00000000..03cb616f --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessageComponent } from './message.component'; + +describe('MessageComponent', () => { + let component: MessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MessageComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/message/message.component.ts b/site-portal/frontend/src/app/components/message/message.component.ts new file mode 100644 index 00000000..cb9102d4 --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { ClarityIcons, successStandardIcon, exclamationCircleIcon, exclamationTriangleIcon, infoCircleIcon } from '@cds/core/icon'; +import { AppService } from '../../app.service' +ClarityIcons.addIcons(successStandardIcon); +ClarityIcons.addIcons(exclamationCircleIcon); +ClarityIcons.addIcons(exclamationTriangleIcon); +ClarityIcons.addIcons(infoCircleIcon); +@Component({ + selector: 'app-message', + templateUrl: './message.component.html', + styleUrls: ['./message.component.css'] +}) + +//This component is for message pop up window +export class MessageComponent implements OnInit { + + constructor(public app: AppService) { + } + public classType: string[] = [] + public classCloseType: string[] = [] + public messageContent = '' + ngOnInit(): void { + this.classType = ['upc-message-' + this.messageType] + this.classCloseType = ['close-' + this.messageType] + } + //message type: 'success', 'info', 'warning', 'hide', 'error' + @Input() messageType: 'success' | 'info' | 'warning' | 'hide' | 'error' = 'info' + @ViewChild('msg') msg!: ElementRef; + public distory() { + this.msg.nativeElement.style.display = 'none' + } +} diff --git a/site-portal/frontend/src/app/components/message/message.module.ts b/site-portal/frontend/src/app/components/message/message.module.ts new file mode 100644 index 00000000..10eb6d22 --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MessageComponent } from './message.component' +import { ClarityModule } from '@clr/angular' +import { ComponentLoaderFactory } from '../loaderFactory' +import { MessageService } from './message.service' +import { TranslateModule } from '@ngx-translate/core' + +@NgModule({ + declarations: [MessageComponent], + imports: [ + CommonModule, + ClarityModule, + TranslateModule + ], + providers: [MessageService, ComponentLoaderFactory], + entryComponents: [MessageComponent], + exports: [MessageComponent] +}) +export class MessageModule { } diff --git a/site-portal/frontend/src/app/components/message/message.service.spec.ts b/site-portal/frontend/src/app/components/message/message.service.spec.ts new file mode 100644 index 00000000..1db761b5 --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + let service: MessageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MessageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/message/message.service.ts b/site-portal/frontend/src/app/components/message/message.service.ts new file mode 100644 index 00000000..9d008e0d --- /dev/null +++ b/site-portal/frontend/src/app/components/message/message.service.ts @@ -0,0 +1,65 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { ComponentLoaderFactory } from '../loaderFactory' +import { ComponentLoader } from '../components-loader' +import { MessageComponent } from './message.component' +@Injectable({ + providedIn: 'root' +}) +export class MessageService { + constructor( + private _clf: ComponentLoaderFactory, + private _injector: Injector, + ) { + this._loader = this._clf.create(); + } + public ref: any + private _loader: ComponentLoader + private popUpMessage(t: string, messageContent: string, duration = 10000) { + this._loader.attch(MessageComponent).to('body') + const opts = { + messageType: t, + messageContent: messageContent + } + this.ref = this._loader.create(opts) + this.ref.changeDetectorRef.markForCheck() + this.ref.changeDetectorRef.detectChanges() + setTimeout(() => { + if (this.ref !== '') { + this._loader.remove(this.ref) + } + }, duration) + } + public info(messageContent: string, duration?: number) { + this.popUpMessage('info', messageContent, duration); + } + public success(messageContent: string, duration?: number) { + this.popUpMessage('success', messageContent, duration); + } + public error(messageContent: string, duration?: number) { + this.popUpMessage('error', messageContent, duration); + } + public warning(messageContent: string, duration?: number) { + this.popUpMessage('warning', messageContent, duration); + } + public close() { + if (this.ref !== '') { + try { + this._loader.remove(this.ref) + this.ref = '' + } catch (error) { + + } + } + } +} diff --git a/site-portal/frontend/src/app/components/sidenav/sidenav.component.css b/site-portal/frontend/src/app/components/sidenav/sidenav.component.css new file mode 100644 index 00000000..a2ca8cd2 --- /dev/null +++ b/site-portal/frontend/src/app/components/sidenav/sidenav.component.css @@ -0,0 +1,7 @@ +.sidenav1{ + width: 250px; + height: 100% +} +.nav-active { + background-color: #ccc; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/sidenav/sidenav.component.html b/site-portal/frontend/src/app/components/sidenav/sidenav.component.html new file mode 100644 index 00000000..ddd57b65 --- /dev/null +++ b/site-portal/frontend/src/app/components/sidenav/sidenav.component.html @@ -0,0 +1,20 @@ + + + {{'nav.projectMg' | translate}}​ + + + {{'nav.dataMg' | translate}} + + + + {{'nav.modelMg' | translate}} + + + + {{'nav.userMg' | translate}}​ + + + {{'nav.siteConfiguration' | translate}} + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/sidenav/sidenav.component.spec.ts b/site-portal/frontend/src/app/components/sidenav/sidenav.component.spec.ts new file mode 100644 index 00000000..13707fe5 --- /dev/null +++ b/site-portal/frontend/src/app/components/sidenav/sidenav.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SidenavComponent } from './sidenav.component'; + +describe('SidenavComponent', () => { + let component: SidenavComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SidenavComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SidenavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/sidenav/sidenav.component.ts b/site-portal/frontend/src/app/components/sidenav/sidenav.component.ts new file mode 100644 index 00000000..3f65134e --- /dev/null +++ b/site-portal/frontend/src/app/components/sidenav/sidenav.component.ts @@ -0,0 +1,42 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +import { Component, OnInit } from '@angular/core'; +import '@cds/core/icon/register.js'; +import { ClarityIcons, cogIcon, dataClusterIcon, gridViewIcon, homeIcon, usersIcon, vmIcon } from '@cds/core/icon'; + +ClarityIcons.addIcons(usersIcon); +ClarityIcons.addIcons(homeIcon); +ClarityIcons.addIcons(dataClusterIcon); +ClarityIcons.addIcons(cogIcon); +ClarityIcons.addIcons(gridViewIcon); +ClarityIcons.addIcons(vmIcon); + +@Component({ + selector: 'app-sidenav', + templateUrl: './sidenav.component.html', + styleUrls: ['./sidenav.component.css'] +}) +export class SidenavComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + condition: boolean = false; + + //trigger side navigation bar opened or closed + toggleDropdown() { + this.condition = !this.condition; + } + collapsed = false; +} diff --git a/site-portal/frontend/src/app/components/upload-file/upload-file.component.css b/site-portal/frontend/src/app/components/upload-file/upload-file.component.css new file mode 100644 index 00000000..6ab2a3f4 --- /dev/null +++ b/site-portal/frontend/src/app/components/upload-file/upload-file.component.css @@ -0,0 +1,45 @@ +.file-wrap { + margin-top: 15px; +} +.progress { + display: flex; + align-items: center; + margin-bottom: 1px; +} +.browse { + border: 1px solid #2779AD; + border-radius: 4px; + padding: 0 10px; + color: #2779AD; +} +.folder-icon { + position: relative; + top: -2px; +} +.browse span { + display: inline-block; + margin-left: 5px; +} +.closeBtn::after { + content: 'X'; + color: #333; + font-size: 20px; + position: relative; + left: 30px; + line-height: 1; +} +.close-x { + font-size: 20px; + color: #999; + border-bottom: none; +} +.close-x:focus { + border: none; + background-size: 0 0; +} +.close-text { + line-height: 24px; + font-size: 12px; + border: none; + color: #42800E; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/components/upload-file/upload-file.component.html b/site-portal/frontend/src/app/components/upload-file/upload-file.component.html new file mode 100644 index 00000000..2c7284ea --- /dev/null +++ b/site-portal/frontend/src/app/components/upload-file/upload-file.component.html @@ -0,0 +1,18 @@ +
+ + + + + + {{'CommonlyUse.success' | translate}} + +
{{'validator.fileSize' | translate | templateReplacement: size}}
+
{{'validator.fileFormat' | translate | templateReplacement: format}}
+
+ + {{progress}}% +
+
diff --git a/site-portal/frontend/src/app/components/upload-file/upload-file.component.spec.ts b/site-portal/frontend/src/app/components/upload-file/upload-file.component.spec.ts new file mode 100644 index 00000000..66e2e7d3 --- /dev/null +++ b/site-portal/frontend/src/app/components/upload-file/upload-file.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UploadFileComponent } from './upload-file.component'; + +describe('UploadFileComponent', () => { + let component: UploadFileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UploadFileComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UploadFileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/components/upload-file/upload-file.component.ts b/site-portal/frontend/src/app/components/upload-file/upload-file.component.ts new file mode 100644 index 00000000..d6dcdc7d --- /dev/null +++ b/site-portal/frontend/src/app/components/upload-file/upload-file.component.ts @@ -0,0 +1,80 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, Input, ViewChild, ElementRef,OnDestroy } from '@angular/core'; +import '@cds/core/icon/register.js'; +import { folderIcon, ClarityIcons } from '@cds/core/icon'; +ClarityIcons.addIcons(folderIcon); +@Component({ + selector: 'app-upload-file', + templateUrl: './upload-file.component.html', + styleUrls: ['./upload-file.component.css'] +}) + +//this component is for uploading file +export class UploadFileComponent implements OnInit,OnDestroy { + constructor() { } + ngOnDestroy(): void { + this.fileName = 'BROWSE' + this.file = '' + this.fileinput.nativeElement.value = null + } + maxSizeFlag = false + fileType = false + isUploaded = false + get size () { + return this.maxSize + 'MB' + } + format = 'csv' + file: any = '' + fileName = 'BROWSE' + @Input() fileList: string[] = [] + @Input() progress = 0 + @Input() maxSize = 500 + @ViewChild('file') fileinput!: ElementRef + ngOnInit(): void { + } + uploadFile(event:Event) { + this.maxSizeFlag = false + this.fileType = false + this.file = (event.target as HTMLInputElement)?.files?.[0]; + this.fileName = this.file.name + // limit file size of 500m + if (this.file?.size) { + const size = this.file.size + const sizeM = size / 1024 / 1024 + if (sizeM < this.maxSize) { + // validate file format + if (this.isCSVFile(this.fileName)) { + this.isUploaded = true + } else { + this.fileinput.nativeElement.value = null + this.fileType = true + } + } else { + this.fileinput.nativeElement.value = null + this.maxSizeFlag = true + } + } + } + empty () { + this.fileName = 'BROWSE' + this.file = '' + this.fileinput.nativeElement.value = null + } + input() { + this.fileinput.nativeElement.click() + } + isCSVFile(fileName:string): boolean { + const suffix = fileName.slice(-4) + return suffix === ".csv" + } +} diff --git a/site-portal/frontend/src/app/service/auth.service.spec.ts b/site-portal/frontend/src/app/service/auth.service.spec.ts new file mode 100644 index 00000000..f1251cac --- /dev/null +++ b/site-portal/frontend/src/app/service/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/auth.service.ts b/site-portal/frontend/src/app/service/auth.service.ts new file mode 100644 index 00000000..2fce4321 --- /dev/null +++ b/site-portal/frontend/src/app/service/auth.service.ts @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }) +}; +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + constructor(private http: HttpClient) { } + + login(username: string, password: string): Observable { + return this.http.post('/user/login', { + username, + password + }, httpOptions); + } + + logout(): Observable { + return this.http.post('/user/logout', { + }, httpOptions); + } +} diff --git a/site-portal/frontend/src/app/service/data.service.spec.ts b/site-portal/frontend/src/app/service/data.service.spec.ts new file mode 100644 index 00000000..38e8d9ec --- /dev/null +++ b/site-portal/frontend/src/app/service/data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DataService } from './data.service'; + +describe('DataService', () => { + let service: DataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/data.service.ts b/site-portal/frontend/src/app/service/data.service.ts new file mode 100644 index 00000000..f0b705d8 --- /dev/null +++ b/site-portal/frontend/src/app/service/data.service.ts @@ -0,0 +1,54 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { DataListResponse, DataElement, DataColumnResponse } from '../view/data-mg/data-mg.component'; +import { DataDetailResponse, Id_Meta } from '../view/data-details/data-details.component'; + +@Injectable({ + providedIn: 'root' +}) +export class DataService { + constructor(private http: HttpClient) { } + + getDataList() { + return this.http.get('/data'); + } + + uploadData(formData: FormData): Observable { + const req = new HttpRequest('POST', '/data', formData, { + reportProgress: true + }) + return this.http.request(req) + } + + getDataDetail(data_id: string) { + return this.http.get('/data/' + data_id); + } + + deleteData(data_id: string): Observable { + return this.http.delete('/data/' + data_id); + } + + downloadDataDetail(data_id: string) { + return this.http.get('/data/' + data_id + '/file', { responseType: 'text' }); + } + + getDataColumn(data_id: string) { + return this.http.get('/data/' + data_id + '/columns'); + } + + putMetaUpdate(putidmeta: any, data_id: string): Observable { + return this.http.put('/data/' + data_id + '/idmetainfo', putidmeta); + } +} diff --git a/site-portal/frontend/src/app/service/model.service.spec.ts b/site-portal/frontend/src/app/service/model.service.spec.ts new file mode 100644 index 00000000..6ee1e2a2 --- /dev/null +++ b/site-portal/frontend/src/app/service/model.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ModelService } from './model.service'; + +describe('ModelService', () => { + let service: ModelService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ModelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/model.service.ts b/site-portal/frontend/src/app/service/model.service.ts new file mode 100644 index 00000000..fd379029 --- /dev/null +++ b/site-portal/frontend/src/app/service/model.service.ts @@ -0,0 +1,48 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ModelListResponse } from '../view/model-mg/model-mg.component'; +import { ModelDetailResponse } from '../view/model-detail/model-detail.component'; + +@Injectable({ + providedIn: 'root' +}) +export class ModelService { + + constructor(private http: HttpClient) { } + + getModelList() { + return this.http.get('/model'); + } + + getModelDetail(uuid: string) { + return this.http.get('/model/' + uuid); + } + + deleteModel(uuid: string): Observable { + return this.http.delete('/model/' + uuid); + } + + getModelSupportedDeploymentType(uuid: string) { + return this.http.get('/model/' + uuid + '/supportedDeploymentTypes'); + } + + publishModel(uuid: string, deployment_type: number, parameters_json: string, service_name: string): Observable { + return this.http.post('/model/' + uuid + '/publish', { + "deployment_type": deployment_type, + "parameters_json": parameters_json, + "service_name": service_name + }); + } +} diff --git a/site-portal/frontend/src/app/service/project.service.spec.ts b/site-portal/frontend/src/app/service/project.service.spec.ts new file mode 100644 index 00000000..c70ac34b --- /dev/null +++ b/site-portal/frontend/src/app/service/project.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ProjectService } from './project.service'; + +describe('ProjectService', () => { + let service: ProjectService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ProjectService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/project.service.ts b/site-portal/frontend/src/app/service/project.service.ts new file mode 100644 index 00000000..1a64f16e --- /dev/null +++ b/site-portal/frontend/src/app/service/project.service.ts @@ -0,0 +1,159 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { DataListResponse } from '../view/data-mg/data-mg.component'; +import { AutoApprove, ParticipantListResponse, ProjectDetailResponse } from '../view/project-details/project-details.component'; +import { ProjectListResponse } from '../view/project-mg/project-mg.component'; + +@Injectable({ + providedIn: 'root' +}) +export class ProjectService { + + constructor(private http: HttpClient) { } + + getProjectList() { + return this.http.get('/project'); + } + + getProjectDetail(uuid: string) { + return this.http.get('/project/' + uuid); + } + + putAutoApprove(autoapprove: AutoApprove, uuid: string): Observable { + return this.http.put('/project/' + uuid + '/autoapprovalstatus', autoapprove) + } + + getParticipantList(uuid: string, all: boolean) { + let params = new HttpParams().set('all', all); + return this.http.get('/project/' + uuid + '/participant', { params: params }); + } + + createProject(auto_approval_enabled: boolean, description: string, name: string): Observable { + return this.http.post('/project', { + auto_approval_enabled, + description, + name + }); + } + + postInvitation(proj_uuid: string, description: string, name: string, party_id: number, uuid: string): Observable { + return this.http.post('/project/' + proj_uuid + '/invitation', { + description, + name, + party_id, + uuid + }); + } + + acceptInvitation(proj_uuid: string): Observable { + return this.http.post('/project/' + proj_uuid + '/join', { + }); + } + + rejectInvitation(proj_uuid: string): Observable { + return this.http.post('/project/' + proj_uuid + '/reject', { + }); + } + + deleteParticipant(proj_uuid: string, party_uuid: string): Observable { + return this.http.delete('/project/' + proj_uuid + '/participant/' + party_uuid); + } + + getAssociatedDataList(uuid: string) { + return this.http.get('/project/' + uuid + '/data'); + } + + getParticipantAssociatedDataList(uuid: string, participant: string) { + let params = new HttpParams().set('participant', participant); + return this.http.get('/project/' + uuid + '/data', { params: params }); + } + + getLocalDataList(uuid: string) { + return this.http.get('/project/' + uuid + '/data/local'); + } + + associateData(proj_uuid: string, data_id: string, name: string): Observable { + return this.http.post('/project/' + proj_uuid + '/data', {data_id, name}); + } + + deleteAssociatedData(proj_uuid: string, data_uuid: string): Observable { + return this.http.delete('/project/' + proj_uuid + '/data/' + data_uuid); + } + + getJobList(uuid: string) { + return this.http.get('/project/' + uuid + '/job'); + } + + createJob(uuid: string, newJobDetail: any): Observable { + return this.http.post('/project/' + uuid + '/job', newJobDetail); + } + + generateJobConfig(newJobDetail: any): Observable { + return this.http.post('/job/conf/create', newJobDetail); + } + + getJobDetail(job_uuid: string) { + return this.http.get('/job/' + job_uuid); + } + + approveJob(job_uuid: string): Observable { + return this.http.post('/job/' + job_uuid + '/approve', {}); + } + + refreshJob(job_uuid: string): Observable { + return this.http.post('/job/' + job_uuid + '/refresh', {}); + } + + rejectJob(job_uuid: string): Observable { + return this.http.post('/job/' + job_uuid + '/reject', {}); + } + + getModelList(uuid: string) { + return this.http.get('/project/' + uuid + '/model'); + } + + getPredictParticipant(predict_model: string) { + let params = new HttpParams().set('modelUUID', predict_model); + return this.http.get('/job/predict/participant', { params: params }); + } + + deleteJob(job_uuid: string,): Observable { + return this.http.delete('/job/' + job_uuid); + } + + downloadPredictJobResult(job_id: string) { + return this.http.get('/job/' + job_id + '/data-result/download', { responseType: 'text' }); + } + + leaveProject(proj_uuid: string): Observable { + return this.http.post('/project/' + proj_uuid + '/leave', {}); + } + + closeProject(proj_uuid: string): Observable { + return this.http.post('/project/' + proj_uuid + '/close', {}); + } + + getAlgorithmData() { + return this.http.get('/job/components'); + } + + getDslAndConf(data: any, type: 'generateDslFromDag' | 'generateConfFromDag') { + if (type === 'generateDslFromDag') { + return this.http.post('/job/' + type, { 'raw_json': JSON.stringify(data.reqData) }); + } else { + return this.http.post('/job/' + type, { 'dag_json': { 'raw_json': JSON.stringify(data.reqData) }, 'job_conf': data.jobDetail }); + } + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/service/site-configure.service.spec.ts b/site-portal/frontend/src/app/service/site-configure.service.spec.ts new file mode 100644 index 00000000..a83558d9 --- /dev/null +++ b/site-portal/frontend/src/app/service/site-configure.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SiteConfigureService } from './site-configure.service'; + +describe('SiteConfigureService', () => { + let service: SiteConfigureService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SiteConfigureService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/site-configure.service.ts b/site-portal/frontend/src/app/service/site-configure.service.ts new file mode 100644 index 00000000..b9f234a4 --- /dev/null +++ b/site-portal/frontend/src/app/service/site-configure.service.ts @@ -0,0 +1,50 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SiteInfoResponse } from 'src/app/service/site.service'; + +@Injectable({ + providedIn: 'root' +}) +export class SiteConfigureService { + + constructor(private http: HttpClient) { } + + putConfigUpdate(siteUpdatedInfo: any) : Observable { + return this.http.put('/site', siteUpdatedInfo); + } + + connectFML(connectInfo:any): Observable { + return this.http.post('/site/fmlmanager/connect', connectInfo); + } + + testFATEFlow(host: string, https: boolean, port: number): Observable { + return this.http.post('/site/fateflow/connect', { + host, + https, + port + }); + } + + testKubeFlow(kubeconfig: string, minio_access_key: string, minio_endpoint: string, minio_region: string, minio_secret_key: string, minio_ssl_enabled:boolean): Observable { + return this.http.post('/site/kubeflow/connect', { + kubeconfig, + minio_access_key, + minio_endpoint, + minio_region, + minio_secret_key, + minio_ssl_enabled + }); + } +} diff --git a/site-portal/frontend/src/app/service/site.service.spec.ts b/site-portal/frontend/src/app/service/site.service.spec.ts new file mode 100644 index 00000000..6017222a --- /dev/null +++ b/site-portal/frontend/src/app/service/site.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SiteService } from './site.service'; + +describe('SiteService', () => { + let service: SiteService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SiteService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/site.service.ts b/site-portal/frontend/src/app/service/site.service.ts new file mode 100644 index 00000000..c49ee039 --- /dev/null +++ b/site-portal/frontend/src/app/service/site.service.ts @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +export interface SiteInfoResponse { + Code: number, + Message: string, + Data: {}, + data?: string +} + +@Injectable({ + providedIn: 'root' +}) +export class SiteService { + + constructor(private http: HttpClient) { } + + getSiteInfo() { + return this.http.get('/site'); + } + + getCurrentUser() { + return this.http.get('/user/current') + } +} diff --git a/site-portal/frontend/src/app/service/user-mg.service.spec.ts b/site-portal/frontend/src/app/service/user-mg.service.spec.ts new file mode 100644 index 00000000..b5d30200 --- /dev/null +++ b/site-portal/frontend/src/app/service/user-mg.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserMgService } from './user-mg.service'; + +describe('UserMgService', () => { + let service: UserMgService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserMgService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/service/user-mg.service.ts b/site-portal/frontend/src/app/service/user-mg.service.ts new file mode 100644 index 00000000..7ff77dd0 --- /dev/null +++ b/site-portal/frontend/src/app/service/user-mg.service.ts @@ -0,0 +1,39 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PermisionResponse, Permission, UserResponse } from '../view/user-mg/user-mg.component'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class UserMgService { + + constructor(private http: HttpClient) { } + + getUser() { + return this.http.get('/user'); + } + + updatePermision(permission: Permission, id: number): Observable { + return this.http.put('/user/' + String(id) + '/permission', permission); + } + + changePassword(curPassword: string, newPassword: string, userId: any): Observable { + return this.http.put('/user/' + String(userId) + '/password', { + "cur_password": curPassword, + "new_Password": newPassword + }); + } +} + diff --git a/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.spec.ts b/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.spec.ts new file mode 100644 index 00000000..2a44977d --- /dev/null +++ b/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.spec.ts @@ -0,0 +1,8 @@ +import { DateFormattingPipe } from './date-formatting.pipe'; + +describe('DateFormattingPipe', () => { + it('create an instance', () => { + const pipe = new DateFormattingPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.ts b/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.ts new file mode 100644 index 00000000..665caaad --- /dev/null +++ b/site-portal/frontend/src/app/shared/shard/date-formatting.pipe.ts @@ -0,0 +1,31 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Pipe, PipeTransform } from '@angular/core'; +import * as moment from 'moment' +@Pipe({ + name: 'dateFormatting', + pure: false +}) +export class DateFormattingPipe implements PipeTransform { + + transform(value: any, ...args: unknown[]): unknown { + let res = '' + const lang = localStorage.getItem('Site-Portal-Language') || 'en' + if (lang === 'zh_CN') { + res = moment(value).format('YYYY年MM月DD日 HH时mm分ss秒') + } else { + res = moment(value).format('ll, LTS') + } + return res; + } + +} diff --git a/site-portal/frontend/src/app/shared/shard/shard.module.ts b/site-portal/frontend/src/app/shared/shard/shard.module.ts new file mode 100644 index 00000000..44db93ba --- /dev/null +++ b/site-portal/frontend/src/app/shared/shard/shard.module.ts @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TemplateReplacementPipe } from './template-replacement.pipe' +import { TranslateModule } from '@ngx-translate/core'; +import { DateFormattingPipe } from './date-formatting.pipe'; +import { HighJsonComponent } from '../../components/high-json/high-json.component' + + +@NgModule({ + declarations: [ + TemplateReplacementPipe, + DateFormattingPipe, + HighJsonComponent + ], + imports: [ + CommonModule, + TranslateModule + ], + exports: [TemplateReplacementPipe, TranslateModule, DateFormattingPipe, HighJsonComponent] +}) +export class ShardModule { } diff --git a/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.spec.ts b/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.spec.ts new file mode 100644 index 00000000..e37ccd4f --- /dev/null +++ b/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TemplateReplacementPipe } from './template-replacement.pipe'; + +describe('TemplateReplacementPipe', () => { + it('create an instance', () => { + const pipe = new TemplateReplacementPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.ts b/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.ts new file mode 100644 index 00000000..0858ca6b --- /dev/null +++ b/site-portal/frontend/src/app/shared/shard/template-replacement.pipe.ts @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'templateReplacement' +}) +export class TemplateReplacementPipe implements PipeTransform { + + transform(value: string, ...args: string[] | number[]): unknown { + if (typeof value !== 'string') { + throw new Error("The type of the value must be a string"); + } + for (let i = 0; i < args.length; i++) { + if (typeof args[i] === 'string' || typeof args[i] === 'number') { + value = value.replace(/d%/, args[i] + '') + } else { + throw new Error("The template replacement value type must be string or numeric"); + } + } + return value; + } + +} diff --git a/site-portal/frontend/src/app/view/data-details/data-details.component.css b/site-portal/frontend/src/app/view/data-details/data-details.component.css new file mode 100644 index 00000000..51d16ff8 --- /dev/null +++ b/site-portal/frontend/src/app/view/data-details/data-details.component.css @@ -0,0 +1,50 @@ + +.content-area .card1 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 200px; + margin-right: 10px; +} + +.content-area .card2 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 130px; + margin-right: 10px; +} +.downloadbtn { + margin-left: 36px; + font-size:x-small; +} +.togglebtn { + margin-bottom: -8px; + margin-left: 50px; +} +.clr-form-control { + margin-top: 6px; +} + +.selectarea { + margin-left: 24px; +} +textarea { + width: 700px; + height: 100px; + margin-top: -10px; +} +clr-datagrid { + margin: 0px; + margin-top: -12px; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.metaalert { + padding: 0px; + margin-left: 12px ; + width: 60%; +} +.card1 { + margin-top: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/data-details/data-details.component.html b/site-portal/frontend/src/app/view/data-details/data-details.component.html new file mode 100644 index 00000000..67d1422c --- /dev/null +++ b/site-portal/frontend/src/app/view/data-details/data-details.component.html @@ -0,0 +1,177 @@ +
+ <<{{'CommonlyUse.back' | translate}} +

{{'dataDetail.dataDetail' | translate}}

+ + +
+
+
+ + +
+
+
    +
  • + {{'CommonlyUse.name' | translate}}: + {{datadetail.name}} +
  • +
  • + {{'CommonlyUse.id' | translate}}: + {{datadetail.data_id}} +
  • +
  • + {{'CommonlyUse.description' | translate}}: + {{datadetail.description}} +
  • +
  • + {{'dataDetail.dataFile' | translate}}: + {{datadetail.filename}} +
  • +
  • + {{'dataDetail.tableName' | translate}}: + {{datadetail.table_name}} +
  • +
  • + {{'dataMg.uploadJobStatus' | translate}}: + {{datadetail.upload_job_status | translate}} +
  • +
+
+
+
+
{{'dataDetail.dataOverview' | translate}}:
+
+ +
    +
  • + {{'dataDetail.sampleSize' | translate}}: + {{datadetail.sample_size}} +
  • +
  • + {{'dataDetail.idMetadata' | translate}}: + + + +
  • +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
  • +

    {{'dataDetail.featureDimensions' | translate}}:

    + +
  • +
+
+
+
+
{{'dataDetail.dataPreview' | translate}}:
+
+ + {{keyvalue}} + + {{sample[keyvalue]}} + + +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/data-details/data-details.component.spec.ts b/site-portal/frontend/src/app/view/data-details/data-details.component.spec.ts new file mode 100644 index 00000000..87967f4e --- /dev/null +++ b/site-portal/frontend/src/app/view/data-details/data-details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataDetailsComponent } from './data-details.component'; + +describe('DataDetailsComponent', () => { + let component: DataDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DataDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/data-details/data-details.component.ts b/site-portal/frontend/src/app/view/data-details/data-details.component.ts new file mode 100644 index 00000000..7c2735b9 --- /dev/null +++ b/site-portal/frontend/src/app/view/data-details/data-details.component.ts @@ -0,0 +1,330 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DataService } from '../../service/data.service'; +import * as fileSaver from 'file-saver'; +import { ClarityIcons, downloadIcon, trashIcon } from '@cds/core/icon'; +import { MessageService } from '../../components/message/message.service' + +ClarityIcons.addIcons(trashIcon, downloadIcon); +export interface DataDetailResponse { + Code: number; + Message: string; + Data: DataDetail; +} +export interface DataDetail { + creation_time: string, + data_id: string, + description: string, + feature_size: number, + features_array: [], + filename: string, + id_meta_info: Id_Meta, + name: string, + preview_array: string, + sample_size: number, + table_name: string, + upload_job_status: string +} +export interface Id_Meta { + id_encryption_type: number, + id_type: number +} + +@Component({ + selector: 'app-data-details', + templateUrl: './data-details.component.html', + styleUrls: ['./data-details.component.css'] +}) +export class DataDetailsComponent implements OnInit, OnDestroy { + + constructor(private route: ActivatedRoute, private dataservice: DataService, + private router: Router, private msg: MessageService) { + this.showDataDetail(); + } + ngOnDestroy(): void { + this.msg.close() + } + openModal: boolean = false; + + ngOnInit(): void { + this.showDataDetail(); + } + + datadetail: DataDetail = { + creation_time: '', + data_id: '', + description: '', + feature_size: 0, + features_array: [], + filename: '', + id_meta_info: { + id_encryption_type: 0, + id_type: 0 + }, + name: '', + preview_array: '', + sample_size: 0, + table_name: '', + upload_job_status: '' + }; + + dataDetailResponse: any; + dataDetailList: DataDetail[] = []; + errorMessage: string = ""; + featureDimensions: string = ""; + previewArray: any; + key: any; + isPageLoading: boolean = true; + isShowDataDetailFailed: boolean = false; + //showDataDetail is to get the Data Detail + showDataDetail() { + const routeParams = this.route.snapshot.paramMap; + const productIdFromRoute = String(routeParams.get('data_id')); + this.dataservice.getDataDetail(productIdFromRoute) + .subscribe((data: DataDetailResponse) => { + this.dataDetailResponse = data; + this.datadetail = this.dataDetailResponse.data; + this.displayMeta(); + this.featureDimensions = this.datadetail.features_array.toString(); + this.previewArray = JSON.parse(this.datadetail.preview_array); + this.isPageLoading = false; + this.key = []; + for (let keyvalue in this.previewArray[0]) { + this.key.push(keyvalue); + } + }, + err => { + this.isPageLoading = false; + this.isShowDataDetailFailed = true; + this.errorMessage = err.error.message; + }); + } + + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + //deleteData is to request for deleting data + deleteData(data_id: string) { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.dataservice.deleteData(data_id) + .subscribe(() => { + this.router.navigate(['/data-management']); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + isDownloadSubmit: boolean = false; + isDownloadFailed: boolean = false; + //downloadData is to request for downloading data + downloadData(data_id: string, data_name: string) { + this.isDownloadSubmit = true; + this.isDownloadFailed = false; + this.dataservice.downloadDataDetail(data_id) + .subscribe((data: any) => { + this.msg.success('serverMessage.download200', 1000) + this.isDownloadSubmit = true; + let blob: any = new Blob([data], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + fileSaver.saveAs(blob, data_name); + }), (error: any) => { + this.isDownloadFailed = true; + this.errorMessage = 'Error downloading the file'; + } + , () => console.info('Data downloaded successfully'); + }; + + metaOnChange: boolean = false; + toggleOnChange: boolean = false; + options: boolean = false; + option1: boolean = false; + option2: boolean = false; + option3: boolean = false; + option5: boolean = false; + option6: boolean = false; + option4: string = ""; + option7: string = ""; + selectoption1: string = "option3"; + selectoption2: string = "option5"; + //selectionOnChange is triggered when the selection of 'ID Metadata' is changed. + selectionOnChange(val: any) { + this.metaOnChange = true; + this.toggleOnChange = !this.toggleOnChange; + if (val == "option2") { + this.option1 = false; + this.option2 = true; + this.option3 = false; + } + if (val == "option1") { + this.option1 = true; + this.option2 = false; + this.option3 = false; + } + if (val == "option3") { + this.option1 = false; + this.option2 = false; + this.option3 = true; + } + if (val == "option5") { + this.option6 = false; + this.option5 = true; + } + if (val == "option6") { + this.option5 = false; + this.option6 = true; + } + } + + //resetMeta is to reset the selection of ID Metadata + resetMeta() { + this.displayMeta(); + this.metaOnChange = false; + this.toggleOnChange = false; + this.stopPut = false; + this.errorpop = false; + } + + //displayMeta() is to get current selection ID metadata + displayMeta() { + if (this.datadetail.id_meta_info === null) { + this.options = false; + return; + } + this.options = true; + this.option2 = false; + this.option6 = false; + if (this.datadetail.id_meta_info.id_type === 0) this.selectoption1 = "option3"; + if (this.datadetail.id_meta_info.id_type === 1) this.selectoption1 = "option1"; + if (this.datadetail.id_meta_info.id_type === 2) { + this.selectoption1 = "option2"; + this.option2 = true; + this.option4 = "IMEI"; + } + if (this.datadetail.id_meta_info.id_type === 3) { + this.selectoption1 = "option2"; + this.option2 = true; + this.option4 = "IDFA"; + } + if (this.datadetail.id_meta_info.id_type === 4) { + this.selectoption1 = "option2"; + this.option2 = true; + this.option4 = "IDFV"; + } + if (this.datadetail.id_meta_info.id_encryption_type === 0) this.selectoption2 = "option5"; + if (this.datadetail.id_meta_info.id_encryption_type === 1) { + this.selectoption2 === "option6"; + this.option6 = true; + this.option7 = "MD5"; + } + if (this.datadetail.id_meta_info.id_encryption_type === 2) { + this.selectoption2 === "option6"; + this.option6 = true; + this.option7 = "SHA25"; + } + } + + stopPut: boolean = false; + errorpop: boolean = false; + selecterrorMessage: string = ""; + //checkMeta() is to validate the selection before save + checkMeta() { + this.stopPut = false; + //if ID Metadata is not enabled + if (!this.options) { + this.put_idmeta = {}; + return; + } + //if ID Metadata is enabled and the selection is changed by user + if (this.options && this.metaOnChange) { + if (this.selectoption1 === "" && this.selectoption2 === "") { + this.put_idmeta = {}; + return; + } + //validate selection + if (this.selectoption1 === "" || this.selectoption2 === "") { + this.selecterrorMessage = "You must select or unselect both ID type and Encryption type."; + this.stopPut = true; + return; + } + //update value based on the selection + if (this.selectoption1 === "option3") { + this.idmeta.id_type = 0; + } else if (this.selectoption1 === "option1") { + this.idmeta.id_type = 1; + } else if (this.selectoption1 === "option2") { + if (this.option4 === "") { + this.stopPut = true; + this.selecterrorMessage = "You must select a option." + } + if (this.option4 === "IMEI") { + this.idmeta.id_type = 2; + } else if (this.option4 === "IDFA") { + this.idmeta.id_type = 3; + } else if (this.option4 === "IDFV") { + this.idmeta.id_type = 4; + } + } + if (this.selectoption2 === "option5") { + this.idmeta.id_encryption_type = 0; + } else if (this.selectoption2 === "option6") { + if (this.option7 === "") { + this.stopPut = true; + this.selecterrorMessage = "You must select a option." + } + if (this.option7 === "MD5") { + this.idmeta.id_encryption_type = 1; + } else if (this.option7 === "SHA25") { + this.idmeta.id_encryption_type = 2; + } + } + this.put_idmeta = this.idmeta; + } + } + + idmeta: Id_Meta = { + id_encryption_type: Number.MAX_SAFE_INTEGER, + id_type: Number.MAX_SAFE_INTEGER + }; + put_idmeta = {}; + isUpdateSubmit: boolean = false; + isUpdateFailed: boolean = false; + //putIDmetaUpdate is to update the selection of ID MetaData + putIDmetaUpdate(data_id: string) { + this.errorpop = false; + this.checkMeta(); + if (this.stopPut) { + this.errorpop = true; + return; + } + this.isUpdateSubmit = true; + this.dataservice.putMetaUpdate(this.put_idmeta, data_id) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isUpdateFailed = true; + }); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} diff --git a/site-portal/frontend/src/app/view/data-mg/data-mg.component.css b/site-portal/frontend/src/app/view/data-mg/data-mg.component.css new file mode 100644 index 00000000..c81b69fd --- /dev/null +++ b/site-portal/frontend/src/app/view/data-mg/data-mg.component.css @@ -0,0 +1,63 @@ +.content-area{ + width: 100%; + min-width: 1080px; +} +.btn3 { + margin-left: 20%; + margin-top:-8.5%; +} +.t1{ + width: 98%; +} +.t2{ + width: 100%; + height: 300%; +} +.t3{ + width: 70%; + line-height: 50%; + height: 36px; + text-align: left; +} +.searchicon { + size: 10%; +} +.searchinput { + margin: -2%; +} +.searchform{ + margin-left: 80%; +} +.btn-icon{ + width: 20px; + height: 20px; + padding-right: 0; + padding-left: 0; + margin-left: 0; + margin-right: 0; + margin-top: 12%; +} +.alert { + padding-left: 0; + padding-top: 0; + padding-bottom: 0; + padding-right: 0; + margin: 0; + +} +.fileIcon { + margin-top: -30px; + margin-left: 40px; +} + +.refreshbtn { + float: right; + margin-right: 10px; +} +.cusbtn { + display:flex; + align-items: center; + height: 36px; + position: absolute; + right:24px; +}; diff --git a/site-portal/frontend/src/app/view/data-mg/data-mg.component.html b/site-portal/frontend/src/app/view/data-mg/data-mg.component.html new file mode 100644 index 00000000..d43cbed8 --- /dev/null +++ b/site-portal/frontend/src/app/view/data-mg/data-mg.component.html @@ -0,0 +1,119 @@ +
+
+

{{'nav.dataMg'| translate}}

+
+
+ +
+ + + + +
+ + + + + + + +
+
+ + {{'CommonlyUse.name' | translate}} + {{'dataMg.dataID' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'dataMg.profile' | translate}} + {{'dataMg.uploadJobStatus' | translate}} + {{'CommonlyUse.action' | translate}} + + {{element.name}} + {{element.data_id}} + {{element.creation_time | dateFormatting}} + {{'dataMg.Featuresize' | translate}}: {{element.feature_size}}    {{'dataMg.Samplesize' | + translate}}: {{element.sample_size}} + {{element.upload_job_status | translate}} + + {{'CommonlyUse.download' | translate}}   {{'CommonlyUse.delete'| translate}} + + {{datalist ? datalist.length : 0}} {{'CommonlyUse.item' | translate}} + + + + + + +
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/data-mg/data-mg.component.spec.ts b/site-portal/frontend/src/app/view/data-mg/data-mg.component.spec.ts new file mode 100644 index 00000000..7ca336fa --- /dev/null +++ b/site-portal/frontend/src/app/view/data-mg/data-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataMgComponent } from './data-mg.component'; + +describe('DataMgComponent', () => { + let component: DataMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DataMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/data-mg/data-mg.component.ts b/site-portal/frontend/src/app/view/data-mg/data-mg.component.ts new file mode 100644 index 00000000..79ca85ec --- /dev/null +++ b/site-portal/frontend/src/app/view/data-mg/data-mg.component.ts @@ -0,0 +1,266 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; +import { UploadFileComponent } from '../../components/upload-file/upload-file.component' +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import '@cds/core/icon/register.js'; +import { addTextIcon, ClarityIcons, searchIcon, uploadIcon } from '@cds/core/icon'; +import '@cds/core/file/register.js'; +import { DataService } from '../../service/data.service'; +import { MessageService } from '../../components/message/message.service' +import * as fileSaver from 'file-saver'; +import { ValidatorGroup } from '../../../config/validators' +import { CustomComparator } from 'src/utils/comparator'; +import { SiteService } from 'src/app/service/site.service'; +import { HttpEvent, HttpEventType, HttpHeaderResponse, HttpResponse } from '@angular/common/http'; + +ClarityIcons.addIcons(searchIcon, addTextIcon, uploadIcon); + +export interface DataListResponse { + code: number; + message: string; + data: DataElement[]; +} +export interface DataColumnResponse { + code: number; + message: string; + data: String[]; +} +export interface DataElement { + creation_time: string; + data_id: string; + feature_size: number; + name: string; + sample_size: number; +} + +@Component({ + selector: 'app-data-mg', + templateUrl: './data-mg.component.html', + styleUrls: ['./data-mg.component.css'] +}) + + +export class DataMgComponent implements OnInit, OnDestroy { + + form: FormGroup; + @ViewChild('file') fileCof !: UploadFileComponent + constructor(private fb: FormBuilder, private dataservice: DataService, private route: ActivatedRoute, + private router: Router, private changeDetectorRef: ChangeDetectorRef, private msg: MessageService, private siteService: SiteService) { + this.showDataList(); + this.form = this.fb.group( + ValidatorGroup([ + { + name: 'name', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'description', + type: [''] + }, + { + name: 'file', + type: [''] + }, + ]) + ); + } + + ngOnInit(): void { + + } + ngOnDestroy(): void { + this.msg.close() + } + + timeComparator = new CustomComparator("creation_time", "string"); + datalistresponse: any; + public datalist: any[] = [] + // Save all data + public storageDataList: any[] = [] + isPageLoading: boolean = true; + isShowDataFailed: boolean = false; + //showDataList is to get the current data list + showDataList() { + this.isPageLoading = true; + this.dataservice.getDataList() + .subscribe((data: DataListResponse) => { + this.datalistresponse = data; + //default descending order + this.datalist = this.datalistresponse.data?.sort((n1: DataElement, n2: DataElement) => { return this.timeComparator.compare(n1, n2) }); + this.storageDataList = this.datalist; + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowDataFailed = true; + this.isPageLoading = false; + this.isPageLoading = false; + }); + } + + //filterDataList is triggered when user set up the filter condition + filterDataList(data: any) { + this.datalist = [] + if (data.searchValue.trim() === '' && data.eligibleList.length < 1) { + this.datalist = this.storageDataList + } else { + data.eligibleList.forEach((el: any) => { + this.datalist.push(el) + }) + } + } + //showFilter is to display the filtered data list + isShowfilter: boolean = false; + showFilter() { + this.isShowfilter = !this.isShowfilter; + } + //refresh button + refresh() { + this.showDataList(); + this.isShowfilter = false; + } + + name: string = ""; + description: string = ""; + uploadfile: string = ""; + openModal = false; + filterSearchValue = ""; + submitted: boolean = false; + loading: boolean = false; + isUploadFailed: boolean = false; + errorMessage: string = ""; + progress = 0 + //uploadData is to submit upload data request + uploadData() { + this.submitted = true; + this.loading = true; + this.form.get('file')?.setValue(this.fileCof.file) + //validate the form and input + if (this.form.get('name')!.value === '') { + this.errorMessage = "Name can not be empty."; + this.isUploadFailed = true; + return; + } + if (!this.fileCof.isUploaded) { + this.errorMessage = "Please upload a file."; + this.isUploadFailed = true; + return; + } + if (!this.form.valid) { + this.errorMessage = 'Invalid information.'; + this.isUploadFailed = true; + return; + } + var formData: any = new FormData(); + formData.append('name', this.form.get('name')!.value); + formData.append('description', this.form.get('description')!.value); + formData.append('file', this.form.get('file')!.value); + this.dataservice.uploadData(formData).subscribe( + (data: HttpEvent | HttpHeaderResponse | HttpResponse) => { + if (data.type === HttpEventType.UploadProgress) { + if (data.total && data.total > 0) { + this.progress = data.loaded / data.total * 100 + } + } + if (data.type === HttpEventType.Response) { + if (data.status === 200) { + this.msg.success('serverMessage.upload200', 1000) + this.isUploadFailed = false; + this.loading = false; + setTimeout(() => { + this.reloadCurrentRoute(); + }, 500) + } else { + this.progress = 0 + } + } + }, + err => { + this.errorMessage = err.error.message; + this.isUploadFailed = true; + + + } + ); + } + openDeleteModal: boolean = false; + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + pendingDataId: string = ''; + //openConfirmModal is triggered to open the modal for user to confirm the deletion + openConfirmModal(data_id: string) { + this.pendingDataId = data_id; + this.openDeleteModal = true; + } + //deleteData is to submit the request to delete tha data + deleteData() { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.dataservice.deleteData(this.pendingDataId) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + isDownloadSubmit: boolean = false; + isDownloadFailed: boolean = false; + //downloadData is to down load data + downloadData(data_id: string, data_name: string) { + this.isDownloadSubmit = true; + this.isDownloadFailed = false; + this.dataservice.downloadDataDetail(data_id) + .subscribe((data: any) => { + let blob: any = new Blob([data], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + fileSaver.saveAs(blob, data_name + '.csv'); + }), (error: any) => { + this.isDownloadFailed = true; + this.errorMessage = 'Error downloading the file'; + } + , () => console.info('Data downloaded successfully'); + }; + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //openUploadDataModal is to open 'update local data modal' + openUploadDataModal() { + this.openModal = true + //send request to confirm the current authenticate is not expired + this.siteService.getCurrentUser().subscribe( + data => { }, + err => { } + ) + } +} +interface FileType { + type: number + total?: number + loaded?: number +} + + + diff --git a/site-portal/frontend/src/app/view/home/home.component.css b/site-portal/frontend/src/app/view/home/home.component.css new file mode 100644 index 00000000..746f4d42 --- /dev/null +++ b/site-portal/frontend/src/app/view/home/home.component.css @@ -0,0 +1,5 @@ +.route { + flex: 1; + height: 100%; + overflow: auto; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/home/home.component.html b/site-portal/frontend/src/app/view/home/home.component.html new file mode 100644 index 00000000..bab181f6 --- /dev/null +++ b/site-portal/frontend/src/app/view/home/home.component.html @@ -0,0 +1,11 @@ +
+ +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/home/home.component.spec.ts b/site-portal/frontend/src/app/view/home/home.component.spec.ts new file mode 100644 index 00000000..2c5a1726 --- /dev/null +++ b/site-portal/frontend/src/app/view/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/home/home.component.ts b/site-portal/frontend/src/app/view/home/home.component.ts new file mode 100644 index 00000000..bd489582 --- /dev/null +++ b/site-portal/frontend/src/app/view/home/home.component.ts @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] +}) +export class HomeComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/site-portal/frontend/src/app/view/job-detail/job-detail.component.css b/site-portal/frontend/src/app/view/job-detail/job-detail.component.css new file mode 100644 index 00000000..63483043 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-detail.component.css @@ -0,0 +1,121 @@ +.content-area { + width: 100%; + min-width: 1080px; +} +.info { + line-height: 100%; + display: flex; +} +.table{ + width: 100%; +} +.table th td{ + text-align: start; +} +.evaltable{ + width: 20%; +} +.info { + display: flex; +} +.content-area h4 { + border-bottom: 1px solid #ccc; + border-top: 1px solid #ccc; + height: 40px; + font-size: 22px; + line-height: 40px; + margin-top: 0px; + padding-left: 15px; +} +.content-area .first{ + border-top: none; +} +.content-area .list { + padding: 12px 15px; + position: relative; +} +.content-area .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 180px; + margin-right: 10px; +} +.content-area .list .list-table p { + font-size: 15px; + margin-top: 8px; +} +.content-area .list .list-table .table { + min-width: 800px; +} +.content-area .model-cnf span:first-of-type{ + width: 260px; +} +.content-area .predict-cnf span:first-of-type{ + width: 270px; +} +.list .last-li { + list-style: none; + display: inline-block; + min-width: 800px; +} +.clr-form-control { + margin-top: 0px; +} +.clr-form-control label { + font-size: 15px; + font-weight: normal; + color: #666; +} +.clr-form-control .clr-textarea { + width: 400px; + height: 200px; + position: relative; + left: 30px; + margin-bottom: 5px; +} +.psi-cnf .list li span:first-of-type { + width: 180px; +} +.model-eva .evaltable, .predict-cnf .table{ + width: 100%; +} +.m-detail { + text-align: right; +} + +.alert-danger { + padding: 0px; + width: 500px; +} +.alert-warning { + padding: 0px; + width: 500px; +} +.alert-info { + padding: 0px; + width: 500px; +} +.predict_result{ + height:500px +} +.pageLoading { + margin-top: 24px; + margin-left: 12px; +} +.downloadbtn { + font-size:x-small; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.program { + list-style-type: none; + height: 400px; + width: 300px; + padding-left: 50px; + position: absolute; + bottom: 60px; + left: 700px; + z-index: 99; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-detail/job-detail.component.html b/site-portal/frontend/src/app/view/job-detail/job-detail.component.html new file mode 100644 index 00000000..691a478e --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-detail.component.html @@ -0,0 +1,372 @@ +
+ <<{{'CommonlyUse.back' | translate}} +
+

{{'jobDetail.jobDetail'|translate}}

+
+ +
+
+ + + + +
+
+ + + + + + + + + + {{'newJob.jobInformation' | translate}} + +
    +
    +
  • + {{'projectDetail.initiator' | translate}}: + {{job.initiating_site_name}} +
  • +
  • + {{'jobDetail.jobName' | translate}}: + {{job.name}} +
  • +
  • + {{'jobDetail.jobDescription' | translate}}: + {{job.description}} +
  • +
  • + {{'jobDetail.jobID' | translate}}: + {{job.uuid}} +
  • +
  • + {{'newJob.type' | translate}}: + {{constantGather('jobtype', job.type).name | translate}} +
  • +
  • + {{'CommonlyUse.createTime' | translate}}: + {{job.creation_time | dateFormatting}} +
  • +
  • + {{'projectDetail.finishTime' | translate}}: + {{job.finish_time | dateFormatting}} + {{'CommonlyUse.null'| translate}} +
  • +
  • + {{'jobDetail.FATEJobID' | translate}}: + {{job.fate_job_id}} + {{'CommonlyUse.null'|translate}} +
  • +
  • + {{'jobDetail.FATEModelName' | translate}}: + {{job.fate_model_name}} + {{'CommonlyUse.null'|translate}} +
  • +
  • + {{'CommonlyUse.status' | translate}}: + {{constantGather('jobstatus', job.status).name + | + translate}} +
  • +
  • + {{'jobDetail.statusMessage' | translate}}: + {{job.status_message}} +
  • +
    +
+
+
+ + {{'newJob.dataConfiguration' | translate}} + +
    + {{'jobDetail.initiatorData' | translate}}: +
    +
  • + {{'jobDetail.initiatorSite' | translate}}: + {{job.initiator_data.providing_site_name}} + ({{job.initiator_data.providing_site_party_id}}) + {{job.initiator_data.providing_site_name}} ({{'newJob.self'|translate}}) + +
  • +
  • + {{'CommonlyUse.status' | translate}}: + {{constantGather('partyStatus', job.initiator_data.site_status).name | translate}} +
  • +
  • + {{'jobDetail.dataName' | translate}}: + {{job.initiator_data.name}} +
  • +
  • + {{'jobDetail.dataDescription' | translate}}: + {{job.initiator_data.description}} +
  • +
  • + {{'dataMg.dataID' | translate}}: + {{job.initiator_data.data_uuid}} +
  • +
  • + {{'jobDetail.dataLabel' | translate}}: + {{job.initiator_data.label_name}} +
  • +

    {{'jobDetail.collaborativeDataFromOtherParticipant' | translate}}:

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{'projectDetail.participant' | translate}}{{'site.partyId' | translate}}{{'CommonlyUse.status' | translate}}{{'jobDetail.dataName' | translate}}{{'dataMg.dataID' | translate}}{{'jobDetail.dataDescription' | translate}}
    {{item.providing_site_name}}{{item.providing_site_name}} ({{'newJob.self'|translate}}){{item.providing_site_party_id}}{{constantGather('partyStatus', item.site_status).name | translate}}{{item.name}}{{item.data_uuid}}{{item.description}}
    +
    +
+
+
+ + {{'newJob.modelConfiguration'|translate}} + +
+
    +
    +
  • + {{'jobDetail.algorithmModuleName'| translate}}: + {{job.training_model_name}} +
  • +
  • + {{'jobDetail.enableValidation'|translate}}: + {{ 'CommonlyUse.true'| translate}} + {{ 'CommonlyUse.false' | translate}} +
  • +
  • + {{'jobDetail.validationDataSize'|translate}}: + {{'jobDetail.validationSize'|translate | templateReplacement : + job.training_validation_percent}} +
  • +
  • + {{'jobDetail.algorithmType'|translate}}: + {{constantGather('jobtrainingtype', job.training_algorithm_type).name | translate}} +
  • +
    +
  • + + + + +
  • +
    +
  • + + + + +
  • +
  • +
    + +
    +
    +
  • +
    +
+
+
+
    +
  • + {{'jobDetail.model'|translate}}: + {{job.predicting_model_uuid}} +
  • +
  • + {{'jobDetail.FATEModelName'|translate}} + {{job.fate_model_name}} + {{'CommonlyUse.null'|translate}} +
  • +
+
+
+
+ + {{'jobDetail.jobResult'|translate}} + +
    +
  • + {{'jobDetail.modelEvaluation'|translate}}: + + + {{'jobDetail.index'|translate}} + {{'jobDetail.metric'|translate}} + + + {{key}} + {{job.result_info.training_result[key]}} + + +
  • + +
  • + {{'jobDetail.predictResult'|translate}}: + {{'jobDetail.output'| translate | templateReplacement : + job.result_info.predicting_result.count}} +
  • + +
  • + {{'jobDetail.intersectRate'|translate}}: + {{job.result_info.intersection_result.intersect_rate > 0 ? + job.result_info.intersection_result.intersect_rate.toFixed(2) : + job.result_info.intersection_result.intersect_rate}} +
  • +
  • + {{'jobDetail.intersectNumber'|translate}}: + {{job.result_info.intersection_result.intersect_number}} {{'jobDetail.only100'|translate}}} +
  • + + {{'jobDetail.index'|translate}} + {{keyvalue}} + + + {{sample_index}} + {{data}} + + + + {{'jobDetail.index'|translate}} + {{keyvalue}} + + + {{sample_index}} + {{data}} + + + +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-detail/job-detail.component.spec.ts b/site-portal/frontend/src/app/view/job-detail/job-detail.component.spec.ts new file mode 100644 index 00000000..d5703218 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { JobDetailComponent } from './job-detail.component'; + +describe('JobDetailComponent', () => { + let component: JobDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ JobDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JobDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/job-detail/job-detail.component.ts b/site-portal/frontend/src/app/view/job-detail/job-detail.component.ts new file mode 100644 index 00000000..9cc335a2 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-detail.component.ts @@ -0,0 +1,217 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HttpClient } from '@angular/common/http' +import { ProjectService } from '../../service/project.service'; +import { constantGather, JOBSTATUS, JOBTYPE } from '../../../config/constant' +import { MessageService } from '../../components/message/message.service' +import * as fileSaver from 'file-saver'; +import Dag from '../../../config/dag' +import '@cds/core/icon/register.js'; +import { checkIcon, ClarityIcons, refreshIcon, timesIcon } from '@cds/core/icon'; + +ClarityIcons.addIcons(refreshIcon, checkIcon, timesIcon); + +@Component({ + selector: 'app-job-detail', + templateUrl: './job-detail.component.html', + styleUrls: ['./job-detail.component.css'] +}) +export class JobDetailComponent implements OnInit, OnDestroy { + + constructor(private route: ActivatedRoute, public http: HttpClient, private projectservice: ProjectService, private router: Router, private msg: MessageService, private cdRef: ChangeDetectorRef) { + } + ngOnDestroy(): void { + this.msg.close() + } + + //default expaned setting of clr-accordion-panel + panelOpen1: boolean = true; + panelOpen2: boolean = false; + _panelOpen3: boolean = false; + panelOpen4: boolean = true; + get panelOpen3() { + return this._panelOpen3 + } + //draw the flow chart + set panelOpen3(value) { + this._panelOpen3 = value + setTimeout(() => { + let dag: any = new Dag(this.job.dsl_json, this.job.conf_json, "#svg-canvas", "#component_info") + dag.tooltip_css = "dagTips"; + dag.Generate(); + dag.Draw(); + }) + } + options: boolean = false; + project: any; + job: any = {}; + dsl: string = ""; + algoconfig: string = ""; + constantGather = constantGather + jobStatus = JOBSTATUS + jobType = JOBTYPE + metrics_key: any; + ngOnInit(): void { + const routeParams = this.route.snapshot.paramMap; + const jobIdFromRoute = String(routeParams.get('jobid')); + this.showJobDetail(jobIdFromRoute); + } + ngAfterViewChecked() { + this.cdRef.detectChanges() + } + + getJobDetailFailed: boolean = false; + errorMessage: string = ""; + predicting_result_list: any = [] + pageLoading: boolean = true; + loadingCompleted: boolean = false; + //showJobDetail is to getthe job detail by job uuid + showJobDetail(job_id: string) { + this.metrics_key = []; + this.predicting_result_list = []; + this.projectservice.getJobDetail(job_id) + .subscribe(data => { + this.job = data.data + this.pageLoading = false; + if (this.job.type === this.jobType.Modeling) { + for (let key in this.job.result_info.training_result) { + this.metrics_key.push(key); + } + } + if (this.job.type === this.jobType.Predict) { + for (let sample of this.job.result_info.predicting_result.data) { + const data_list = []; + for (let data of sample) { + if (data === null) { + data_list.push('N/A'); + } else if (data === Object(data)) { + data_list.push(JSON.stringify(data)); + } else { + data_list.push(data); + } + } + this.predicting_result_list.push(data_list); + } + } + }, + err => { + this.getJobDetailFailed = true; + this.pageLoading = false; + this.errorMessage = err.error.message; + } + ); + } + + //back button + back() { + const routeParams = this.route.snapshot.paramMap; + const projIdFromRoute = String(routeParams.get('projid')); + this.router.navigate(['project-management', 'project-detail', projIdFromRoute, 'job']); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //actions + approveJobFailed: boolean = false; + approveJobsSubmit: boolean = false; + //approve is to approve the job that pending on current site + approve(job_uuid: string) { + this.approveJobsSubmit = true; + this.approveJobFailed = false; + this.projectservice.approveJob(job_uuid) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.approveJobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + refreshJobFailed: boolean = false; + refreshJobsSubmit: boolean = false; + //refresh job button + refresh(job_uuid: string) { + this.refreshJobsSubmit = true; + this.projectservice.refreshJob(job_uuid) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.refreshJobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + openRejectModal: boolean = false; + rejectJobFailed: boolean = false; + rejectJobsSubmit: boolean = false; + rejectErrorMessage: any; + //reject is to reject the job that pending on current site + reject(job_uuid: string) { + this.rejectJobsSubmit = true; + this.projectservice.rejectJob(job_uuid) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.rejectJobFailed = true; + this.rejectErrorMessage = err.error.message; + } + ); + } + + openDeleteModal: boolean = false; + submitDeleteFailed: boolean = false; + deleteJobSubmit: boolean = false; + deleteerrorMessage: any; + //deleteJob is to submit 'delete job' request + deleteJob(job_uuid: string) { + this.deleteJobSubmit = true; + this.projectservice.deleteJob(job_uuid) + .subscribe(() => { + this.back(); + }, + err => { + this.submitDeleteFailed = true; + this.deleteerrorMessage = err.error.message; + }); + } + + isDownloadSubmit: boolean = false; + isDownloadFailed: boolean = false; + //downloadPredictResult is to download the result of prediction job + downloadPredictResult(job_id: string, job_name: string) { + this.isDownloadSubmit = true; + this.isDownloadFailed = false; + this.projectservice.downloadPredictJobResult(job_id) + .subscribe((data: any) => { + this.isDownloadSubmit = true; + let blob: any = new Blob([data], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + fileSaver.saveAs(blob, job_name); + }), (error: any) => { + this.isDownloadFailed = true; + this.errorMessage = 'Error downloading the file'; + } + }; +} diff --git a/site-portal/frontend/src/app/view/job-detail/job-detail.module.ts b/site-portal/frontend/src/app/view/job-detail/job-detail.module.ts new file mode 100644 index 00000000..fadf8272 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-detail.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { ClarityModule } from '@clr/angular'; +import { FormsModule } from '@angular/forms'; +import { JobDetailsRoutingModule } from './job-details-routing' +import { CommonModule } from '@angular/common'; +import { JobDetailComponent } from './job-detail.component' +import { ShardModule } from 'src/app/shared/shard/shard.module' +@NgModule({ + declarations: [ + JobDetailComponent + ], + imports: [ + CommonModule, + JobDetailsRoutingModule, + ClarityModule, + FormsModule, + ShardModule + ], +}) +export class JobDetailModule {} diff --git a/site-portal/frontend/src/app/view/job-detail/job-details-routing.ts b/site-portal/frontend/src/app/view/job-detail/job-details-routing.ts new file mode 100644 index 00000000..b15f29cf --- /dev/null +++ b/site-portal/frontend/src/app/view/job-detail/job-details-routing.ts @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { JobDetailComponent } from './job-detail.component'; +const routes: Routes = [ + { + path: '', + component: JobDetailComponent + } +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class JobDetailsRoutingModule { } \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-new/job-new-routing.ts b/site-portal/frontend/src/app/view/job-new/job-new-routing.ts new file mode 100644 index 00000000..9988d7a4 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new-routing.ts @@ -0,0 +1,26 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { JobNewComponent } from './job-new.component'; +const routes: Routes = [ + { + path:'', + component: JobNewComponent + } +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class JobNewRoutingModule { } \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-new/job-new.component.html b/site-portal/frontend/src/app/view/job-new/job-new.component.html new file mode 100644 index 00000000..17694710 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.component.html @@ -0,0 +1,573 @@ +
+ <<{{'CommonlyUse.back' | translate}} +

{{'newJob.createNewJob' | translate}}

+
+
+
+
+
+

{{'newJob.jobInformation' | translate}}

+
+
    +
  • + + + + + + + + + + + + + + + +
  • +
  • + + + + + {{form.get('name')?.errors?.emptyMessage || form.get('name')?.errors?.message | + translate}} + {{'CommonlyUse.few' | + translate}}{{form.get('name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + {{'CommonlyUse.many' | + translate}}{{form.get('name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + +
  • +
  • + + + + +
  • +
+
+
+
+
+

{{'newJob.modelConfiguration' | translate}}

+
+
    +
  • + + + + {{'validator.empty' | translate}} + +
  • +
  • + +
  • +
+
+
+
+
+

{{'newJob.dataConfiguration' | translate}}

+
+
+ {{'newJob.editParticipantData' | translate}} + + {{'projectDetail.participant' | translate}} + {{'newJob.associatedData' | translate}} + {{'newJob.labelColumn' | translate}} + + {{displaySelf.name}} ({{'newJob.self' | translate}}) + + {{displaySelf.associated_data.split('+')[0]}} + {{'newJob.pleaseSelect' | translate}} + + {{displaySelf.label_column}} + + {{'newJob.pleaseSelect' | translate}} + + + + {{party.name}} + {{party.associated_data.split('+')[0]}} + + + +
+
+ {{'newJob.editParticipantData' | translate}} + + {{'projectDetail.participant' | translate}} + {{'newJob.associatedData' | translate}} + + {{displaySelf.name}} ({{'newJob.self' | translate}}) + + {{displaySelf.associated_data.split('+')[0]}} + {{'newJob.pleaseSelect' | translate}} + + + + {{party.name}} + {{party.associated_data.split('+')[0]}} + + {{'newJob.pleaseSelect' | translate}} + + + +
+
+
+
+
+

{{'newJob.modelConfiguration' | translate}}

+
+
    +
  • + + + + + {{form.get('model_name')?.errors?.emptyMessage || + form.get('model_name')?.errors?.message | translate}} + + {{'CommonlyUse.few' | + translate}}{{form.get('model_name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + + {{'CommonlyUse.many' | + translate}}{{form.get('model_name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + +
  • +
  • + + + + {{'validator.empty' | translate}} + +
  • +
  • + + + + {{ 'validator.zeroToHundred' + | translate}} + +
  • +
  • +
    + + +
    + + +
    +
    +
    +
    + + {{group.groupName}} +
    +
    +

    + {{c.moduleName}} +

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +

    {{dag.attrForm.moduleName}}

    + + + + + + + + + + +
    + + +
    +
    +
    + +
    +
    + +
    +
    + + + + + + + + +
    + +
    +
    +
    + +
    +
    +
    + +
  • + +
  • + + + + + + + {{'validator.empty' | translate}} + + + + + + + + + + {{'validator.empty' | translate}} + + + + +
  • + +
  • +
    +
    + + + + {{'validator.empty' | translate}} + + +
    +
    + +
    +
    + + + + {{'validator.empty' | + translate}} + + +
    +
    + +
    +
    +
    +
  • + + + +
    +
  • +
+ + +
+
+ +
+
+ + + + {{'CommonlyUse.creating' | translate}}... + +
+
+ + + + + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-new/job-new.component.scss b/site-portal/frontend/src/app/view/job-new/job-new.component.scss new file mode 100644 index 00000000..2e27b1e3 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.component.scss @@ -0,0 +1,325 @@ +.modal-body ::ng-deep{ + .clr-form-control { + margin-top: 0px; + } +} +.card::ng-deep{ + .list-group { + .list-group-item { + &.border { + border-bottom: none !important; + .btnPrimary { + background-color: #2572A3; + color: #fff; + } + } + &.bottom { + border: none; + .info { + display: flex; + min-width: 300px; + align-items: center; + justify-content: space-between; + } + } + &.top { + border: none; + border: none; + .info { + display: flex; + min-width: 300px; + align-items: center; + justify-content: space-between; + } + } + &.json-textarea { + display: flex; + .left { + width: 50%; + .clr-textarea { + width: 100%; + max-width: 100%; + height: 300px; + } + .app-high { + width: calc(100% - 15px); + margin-left:15px; + height: 300px; + box-sizing: border-box; + overflow: auto; + border: 1px solid #b3b3b3; + border-radius: 2px; + } + } + .right { + width: 50%; + height: 800px; + margin-top: 10px; + position: relative; + z-index: 99; + .program { + width: 100%; + height: 800px; + list-style-type: none; + .svg-canvas { + margin: 0 auto; + } + } + } + } + } + } +} +.drag-session { + height: 820px; + display: flex; + flex: 1; + position: relative; + .component::ng-deep { + border: 1px solid #333; + width: 240px; + margin-right: 10px; + overflow: auto; + .angle-h5 { + margin-top: 0px; + position: relative; + z-index: 1; + border-bottom: 1px solid #ddd; + color: #666; + overflow:hidden; + white-space: nowrap; + text-overflow: ellipsis; + &.bac { + background-color: #D8E3E9; + } + } + .angle-p { + margin-top: 0px; + padding-left: 20px; + color: #777; + border-bottom: 1px solid #ddd; + cursor: move; + } + .angle { + transform: rotate(90deg); + background-color: transparent; + } + .show { + opacity: 1; + .cdk-drag { + height: 25px; + } + } + .hidden { + opacity: 0; + .cdk-drag { + height: 0px; + } + } + + } + .attrbutes::ng-deep { + width: 280px; + border: 1px solid #333; + overflow: auto; + position: relative; + h4 { + margin-top: 20px; + font-size: 16px; + text-align: center; + } + .radio::ng-deep { + display: block; + width: 260px; + + margin: 0px auto; + .clr-control-container { + justify-content: space-between; + padding: 0px; + max-width: 100%; + .clr-radio-wrapper { + label { + width: 90px; + padding-left: 20px; + word-break: normal; + } + } + } + } + .btn-group { + position: absolute; + left: 50%; + transform: translateX(-50%); + .bgc { + background-color: #2572A3; + color: #fff; + } + .plus { + width:40px; + min-width: 40px; + padding: 0px; + } + } + .clr-control-label { + padding: 0; + font-weight: normal; + width: 100px; + &.radio-label { + padding-left: 25px; + } + + } + input { + width: 100%; + } + .clr-form { + text-align: left; + } + } + .textare { + flex: 1; + min-width: 900px; + border: 1px solid #333; + padding-top: 32px; + margin-right: 10px; + overflow: auto; + position: relative; + } + .zoom { + position: absolute; + left: 255px; + top: 2px; + z-index: 99; + button { + width: 30px; + height: 30px; + border: none; + margin-right: 10px; + background: url(../../../assets/zoom.png) -3px; + background-size: cover; + &:nth-of-type(2) { + background: url(../../../assets/zoom.png) -37px; + background-size: cover; + } + } + + } +} +.text-area::ng-deep{ + padding: 0 20px; + .tab-content { + .clr-form-control { + margin-top: 0px; + padding: 0 10px; + display: block; + .clr-control-label { + width: 0px; + margin: 0px; + flex:0; + } +\ + .clr-control-container { + display: block; + padding: 0px; + max-width: 100%; + .clr-textarea { + width: 100%; + height: 100px; + overflow: auto; + background-color: rgba(239, 239, 239, 0.3); + margin-bottom: 8px; + } + } + } + .textarea { + width: 100%; + height: 100px; + margin-top: 8px; + padding: 8px 12px; + border-radius: 2px; + overflow: auto; + background-color: rgba(239, 239, 239, 0.3); + } + } + } +.modal { + .modal-content { + width: 800px; + .plus-circle{ + float: none; + } + .input-item { + border: 1px solid #666; + padding-left: 15px; + padding-bottom: 10px; + } + .title { + span { + display: inline-block; + width: 300px; + font-weight: 700; + margin-bottom: 10px; + } + } + .opt { + display: flex; + label { + width: 46px; + } + span { + display: inline-block; + border-bottom: 1px solid; + } + .trash { + position: relative; + top: 16px; + } + .select-item { + position: relative; + &.module { + width: 300px; + } + &.module::ng-deep { + label { + width: auto; + margin-right: 5px; + } + .clr-form-control { + margin-right: 10px; + .clr-select-wrapper { + width: 100px; + .clr-select { + width: 100px; + } + } + } + } + &:last-of-type { + width:auto + } + } + .plus-circle { + position: relative; + top: 8px; + } + } + .btns { + display: flex; + justify-content: flex-end; + } + } +} +.clr-control-container-select { + &::ng-deep { + label { + width: 0px; + margin: 0px; + max-width: 0px; + &.clr-control-label { + width: 0px; + margin: 0px; + max-width: 0px; + } + } + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-new/job-new.component.spec.ts b/site-portal/frontend/src/app/view/job-new/job-new.component.spec.ts new file mode 100644 index 00000000..928619ff --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { JobNewComponent } from './job-new.component'; + +describe('JobNewComponent', () => { + let component: JobNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ JobNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JobNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/job-new/job-new.component.ts b/site-portal/frontend/src/app/view/job-new/job-new.component.ts new file mode 100644 index 00000000..7646707f --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.component.ts @@ -0,0 +1,1804 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit, ChangeDetectorRef } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DataService } from '../../service/data.service'; +import { ParticipantListResponse } from '../project-details/project-details.component'; +import { ProjectService } from '../../service/project.service'; +import { MessageService } from '../../components/message/message.service' +import { ValidatorGroup } from '../../../config/validators' +import Dag from '../../../config/dag-drag' +import DagJson from '../../../config/dag' +import '@cds/core/icon/register.js'; +import { plusCircleIcon, ClarityIcons, angleIcon } from '@cds/core/icon'; +import { HighJsonComponent } from 'src/app/components/high-json/high-json.component' +ClarityIcons.addIcons(plusCircleIcon, angleIcon); + +export interface PartyUser { + creation_time: string, + description: string, + name: string, + party_id: number, + status: number, + uuid: string, + selected: boolean, + associated_data: string, + data_list: any, + label_column: string +} +export interface PredictModel { + component_name: string, + create_time: string, + job_name: string, + job_uuid: string, + model_id: string, + model_version: string, + name: string, + party_id: 0, + project_name: string, + project_uuid: string, + role: string, + uuid: string, + participant: PartyUser[], + selected: boolean, +} +interface HostType { + name: string + form: any +} +interface AlgorithmType { + moduleName: string + parameters: { + [key: string]: any + } + conditions: { + possible_input: string[], + can_be_endpoint: boolean, + }, + input: { + data: string[], + model: string[] + } + output: { + data: string[], + model: string[] + } + count?: number +} +interface GroupType { + groupName: string, + modules: AlgorithmType[] + angle?: boolean +} +interface programModol { + name: string + value: boolean | string | number +} +interface optRelationshipsType { + value: string + type: string + relation?: string, + list: any[] +} +interface SvgModel { + [key: string]: { + module: string + parameters: { [key: string]: any }, + attributes: { + [key: string]: { + } + } + conditions: { + output?: { + data: string[], + model: string[] + }, + input?: { + data: string[] + }, + relation?: any[] + }, + attributeType: 'common' | 'diff', + diffAttribute?: any, + default?: any + } +} +interface InputOrOutputTYpe { + data: string[] + model: string[] +} +interface Relationship { + data: optRelationshipsType[], + model: optRelationshipsType[] +} +interface inputModuleType { + inputModule: string + optRelationships: optRelationshipsType[] + inputTypeList: string[] +} + +@Component({ + selector: 'app-job-new', + templateUrl: './job-new.component.html', + styleUrls: ['./job-new.component.scss', './job-new.css'] +}) +export class JobNewComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild('dslJson') dslRef!: HighJsonComponent; + @ViewChild('alJson') alRef!: HighJsonComponent; + + //drag and dag module + dropOrJson = true // drop + algorithmDataSourc: GroupType[] = [] + algorithmList: AlgorithmType[] = [] + dragStorageArea: any[] = [] + + // Data used to generate structure diagrams + svgData: SvgModel = { + reader_0: { + module: "Reader", + attributes: {}, + parameters: {}, + conditions: { + output: { + data: ['data'], + model: [] + } + }, + attributeType: 'common' + } + } + + currentDragObj: any = {}; + + // drag mode d3 instance + dag: any = {} + // json mode d3 instance + dagJson: any = {} + + dslJson: string = '' + confJson: string = '' + + // drag mode The current module can input list + inputModuleList: inputModuleType[] = [{ + inputModule: '', + optRelationships: [{ + value: '', + type: '', + relation: '', + list: [] + }], + inputTypeList: [] + }] + outputRelationships: any[] = [] + JobAlgorithmType = 0 + addModuleFlag = false + currentModuleAttrForm: any = {} + dblModuleName = '' + // current difference attribute + diff = -1 + // host group + hostList: HostType[] = [] + + form: FormGroup; + _$: any + openModal: boolean = false; + modalSize = "xl"; + program: programModol[] = [] + name: string = ""; + desc: string = ""; + validationDataPercent: string = ""; + model_name: string = ""; + algorithm: string = ""; + algorithmConfig: string = ""; + dsl: string = ""; + options: boolean = true; + + //job type variable + psi: boolean = false; + modeling: boolean = true; + predict: boolean = false; + + participant: string = ""; + newJobType: string = "modeling"; + dataOptions: any; + columnOptions: any; + selected: any; + errorMessage: string = ""; + + routeParams = this.route.snapshot.paramMap; + projectUUID = String(this.routeParams.get('id')); + allParticipantList: any = []; + participantList: PartyUser[] = []; + isShowParticiapantFailed: boolean = false; + selfAssociatedDataListResponse: any; + getParticipantAssociatedDataListIsPending: boolean = false; + + //displayParticipant is displayed data configuration of paticipant excepting 'Self' + displayParticipant: PartyUser[] = []; + //displaySelf is displayed data configuration of 'self' paticipant + displaySelf: PartyUser = { + creation_time: "", + description: "", + name: "", + party_id: 0, + status: 0, + uuid: "", + selected: true, + associated_data: "", + data_list: [], + label_column: "" + }; + submitSaveSelection: boolean = false; + invalidSave: boolean = false; + noAssociatedData: boolean = false; + showLocalDataListFailed: boolean = false; + showAssociatedDataSubmit: boolean = false; + allAssociatedData: any + modalErrorMessage: string = ''; selfdatalist: any; self: PartyUser = { + creation_time: "", + description: "", + name: "", + party_id: 0, + status: 0, + uuid: "", + selected: true, + associated_data: "", + data_list: [], + label_column: "" + } + dataColumnResponse: any; + dataColumn: any; + jobDetail: any = { + conf_json: "", + description: "", + dsl_json: "", + initiator_data: { + data_uuid: "", + label_name: "" + }, + name: "", + other_site_data: [], + predicting_model_uuid: "", + project_uuid: "", + training_algorithm_type: 1, + training_component_list_to_deploy: [] as string[], + training_model_name: "", + training_validation_enabled: true, + training_validation_percent: 0, + type: 0 + } + //predictModel is selected model uuid for prediction job + predictModel: string = ""; + submitNewJobFailed: boolean = false; + submitNewJob: boolean = false; + formvalue: any; + submitGeneratedFailed: boolean = false; + submitGenerated: boolean = false; + isShowModelFailed: boolean = false; + modelList: any; + displayPredictParticipant: any = []; + predictParticipantList: any; + newPredictParticipantList: PartyUser[] = []; + // Automatically calculate the width of the artboard in drag mode + get width() { + if (this.dag.count > 3) { + return (this.dag.count - 3) * 200 + } + return 0 + } + // Automatically calculate the Height of the artboard in drag mode + get height() { + if (this.dag.level > 10) { + return (this.dag.level - 10) * 90 + } + return 0 + } + // Dynamically get the output options of the current module + get changeOptRelationshipList() { + const keyList: string[] = [] + if (this.currentDragObj.hasOwnProperty('default')) { + this.currentDragObj.default.conditions.possible_input.forEach((str: string) => { + for (const key in this.svgData) { + if (this.svgData[key].module === str) { + keyList.push(key) + } + } + }) + } else { + this.currentDragObj.conditions.possible_input.forEach((str: string) => { + for (const key in this.svgData) { + if (this.svgData[key].module === str) { + keyList.push(key) + } + } + }) + } + return keyList + } + // create job button disabled + get disabled() { + return this.inputModuleList.every(item => { + return item.optRelationships.every(el => el.value !== '' && el.type !== '' && el.relation !== '') + }) && this.outputRelationships.every(el => el.value !== '' && el.type !== '') + } + // Toggle module property sheet representation in drag and drop mode + get currentDiff() { + return this.diff + } + set currentDiff(value) { + if (value > -1) { + // when click the module + if (this.hostList[value].form && JSON.stringify(this.hostList[value].form) !== '{}') { + // existing host + this.currentModuleAttrForm = {} + for (const key in this.hostList[value].form) { + if (this.svgData[this.dag.attrForm.moduleName].diffAttribute) { + const obj = JSON.parse(JSON.stringify(this.svgData[this.dag.attrForm.moduleName].diffAttribute['host_'+value])) + + if (Object.prototype.toString.call(this.hostList[value].form[key]) === '[object Object]') { + const result = this.hostList[value].form[key].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[key] = obj[key] + } else { + this.currentModuleAttrForm[key] = JSON.stringify(obj[key]) + } + } else if (Object.prototype.toString.call(this.hostList[value].form[key]) === '[object Array]') { + this.currentModuleAttrForm[key] = JSON.stringify(obj[key]) + } else { + this.currentModuleAttrForm[key] = obj[key] + } + } else { + if (Object.prototype.toString.call(this.hostList[value].form[key]) === '[object Object]') { + const result = this.hostList[value].form[key].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[key] = this.hostList[value].form[key] + } else { + this.currentModuleAttrForm[key] = JSON.stringify(this.hostList[value].form[key]) + } + } else if (Object.prototype.toString.call(this.hostList[value].form[key]) === '[object Array]') { + this.currentModuleAttrForm[key] = JSON.stringify(this.hostList[value].form[key]) + } else { + this.currentModuleAttrForm[key] = this.hostList[value].form[key] + } + } + + } + } else { + //dag and drag mode: the first host + if (value === 0) { + this.currentModuleAttrForm = {} + this.dag.attrForm.attrList.forEach((el: any) => { + const obj = JSON.parse(JSON.stringify(el)) + if (Object.prototype.toString.call(obj.value) === '[object Object]') { + const result = obj.value.hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[el.name] = obj.value + } else { + this.currentModuleAttrForm[el.name] = JSON.stringify(obj.value) + } + + } else if (Object.prototype.toString.call(obj.value) === '[object Array]') { + this.currentModuleAttrForm[el.name] = JSON.stringify(obj.value) + } else { + this.currentModuleAttrForm[el.name] = obj.value + } + }); + this.hostList[0] = { + name: 'host_0', + form: JSON.parse(JSON.stringify(this.currentModuleAttrForm)) + } + } + } + } else {// when it is guest + if (this.dag.attrForm.diffList.length > 0) { + this.dag.attrForm.diffList.forEach((el: any) => { + if (el.name === 'guest') { + this.currentModuleAttrForm = {} + for (const key in el.form) { + if (Object.prototype.toString.call(el.form[key]) === '[object Object]') { + const result = el.form[key].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[key] = el.form[key] + } else { + this.currentModuleAttrForm[key] = JSON.stringify(el.form[key]) + } + } else if (Object.prototype.toString.call(el.form[key]) === '[object Array]') { + this.currentModuleAttrForm[key] = JSON.stringify(el.form[key]) + } else { + this.currentModuleAttrForm[key] = el.form[key] + } + } + } + }); + } else { + if (this.svgData[this.dag.attrForm.moduleName].diffAttribute) { + this.currentModuleAttrForm = {} + const obj = JSON.parse(JSON.stringify(this.svgData[this.dag.attrForm.moduleName].diffAttribute.guest)) + for (const key in obj) { + if (Object.prototype.toString.call(obj[key]) === '[object Object]') { + const result = obj[key].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[key] = obj[key] + } else { + this.currentModuleAttrForm[key] = JSON.stringify(obj[key]) + } + + } else if (Object.prototype.toString.call(obj[key]) === '[object Array]') { + this.currentModuleAttrForm[key] = JSON.stringify(obj[key]) + } else { + this.currentModuleAttrForm[key] = obj[key] + } + } + } + } + } + this.diff = value + } + // Decide whether to enable different radio buttons according to the number of Data Configuration + get diffShow() { + if (this.dag.attrForm.moduleName === 'reader_0') { + return true + } + return this.displayParticipant.filter(el => el.selected).length > 0 + + } + + constructor(private fb: FormBuilder, private route: ActivatedRoute, private projectservice: ProjectService, private router: Router, private dataservice: DataService, private msg: MessageService, private cdRef: ChangeDetectorRef) { + + this.form = this.fb.group( + ValidatorGroup([ + { + name: 'name', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'desc', + type: [''] + }, + { + name: 'validationDataPercent', + type: ['notRequired', 'zero'] + }, + { + name: 'newJobType', + type: [''] + }, + { + name: 'model_name', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'algorithmConfig', + type: [''] + }, + { + name: 'algorithm', + type: [''] + }, + { + name: 'dsl', + type: [''] + }, + { + name: 'predictModel', + type: [''] + }, + ]) + ); + } + + + ngOnInit(): void { + this.showParticipantList(); + this.getDataSourc() + this.dag = new Dag(this.svgData, '#svg-canvas-drop', this.moduleAttrSave, this) + this.startDag('{}', '{}') + this.dag.Generate() + this.program = [] + setTimeout(() => { + this.dag.Draw() + this.dropOrJson = false + }) + } + ngOnDestroy(): void { + //leave close message + this.msg.close() + } + + ngAfterViewInit(): void { + this.cdRef.detectChanges(); + } + addOptRelationship(item: inputModuleType) { + item.optRelationships.push({ + value: item.inputModule, + type: '', + relation: '', + list: [] + }) + } + addInputModule() { + this.inputModuleList.push({ + inputModule: '', + optRelationships: [{ + value: '', + type: '', + relation: '', + list: [] + }], + inputTypeList: [] + }) + } + deleteItem(index: number, item: inputModuleType | 'inputModuleList') { + if (item === 'inputModuleList') { + this[item].splice(index, 1) + } else { + item.optRelationships.splice(index, 1) + } + } + // Drag and drop an algorithm module to trigger a function + dragHandler(e: any) { + // get drag and dag object + this.currentDragObj = e.previousContainer.data[e.previousIndex] as AlgorithmType + this.inputModuleList = [{ + inputModule: '', + optRelationships: [{ + value: '', + type: '', + relation: '', + list: [] + }], + inputTypeList: [] + }] + if (e.previousContainer.data[e.previousIndex].moduleName === 'HomoLR') { + this.JobAlgorithmType = 1 + } else if (e.previousContainer.data[e.previousIndex].moduleName === 'HomoSecureboost') { + this.JobAlgorithmType = 2 + } + // true Indicates a new drag and drop + this.bulletFrame(true, '') + } + // handle pop-up modal + bulletFrame(bool: boolean, str: string) { + this.dblModuleName = str + if (bool) { + // get the available input list + this.outputRelationships = [] + //Get the output item of the current drag module + this.getOutputListHandler(this.currentDragObj.output) + } + else { + this.inputModuleList = [] + const input = this.svgData[str].conditions.input + if (input) { + input.data.forEach(el => { + this.inputModuleList.push({ + inputModule: el, + optRelationships: [], + inputTypeList: [] + }) + }) + } + this.currentDragObj = this.svgData[str] + this.currentDragObj.moduleName = this.currentDragObj.module + this.inputModuleList.forEach(item => { + if (this.svgData[item.inputModule]) { + const relation = this.svgData[str].conditions.relation + const moudel = this.svgData[item.inputModule].default + if (moudel) { + if (moudel.output.model.length > 0) { + item.inputTypeList = [...moudel.output.model, ...moudel.output.data] + } else { + item.inputTypeList = [...moudel.output.data] + } + } else { + item.inputTypeList = ['data'] + } + if (relation) { + for (const key in relation) { + if (key === item.inputModule) { + relation[key].list.forEach((el: any) => { + if (el.value === 'model') { + item.optRelationships.push({ + value: item.inputModule, + type: el.key, + relation: el.value, + list: this.currentDragObj.default.input.model + }) + } else { + item.optRelationships.push({ + value: item.inputModule, + type: el.key, + relation: el.value, + list: this.currentDragObj.default.input.data + }) + } + }) + } + } + } + this.outputRelationships = [] + } else { + item.optRelationships[0] = { + value: '', + relation: '', + type: '', + list: [] + } + item.inputModule = '' + } + }) + // get the available output list + this.getOutputListHandler(this.currentDragObj.conditions) + } + this.addModuleFlag = true + } + currentModel(item: inputModuleType) { + let moudel: any = {} + for (const data in this.svgData) { + if (data === item.inputModule) { + moudel = this.svgData[data] + } + } + if (moudel) { + if (moudel.conditions.output.model.length > 0) { + item.inputTypeList = ['', ...moudel.conditions.output.model, ...moudel.conditions.output.data] + } else { + item.inputTypeList = ['', ...moudel.conditions.output.data] + } + } else { + item.inputTypeList = ['', 'data'] + } + } + optChange(item: inputModuleType, opt: optRelationshipsType, type?: string) { + opt.value = item.inputModule + if (this.currentDragObj.input) { + if (type === 'model') { + opt.list = this.currentDragObj.input.model + } else { + opt.list = this.currentDragObj.input.data + } + } else { + if (type === 'model') { + opt.list = this.currentDragObj.default.input.model + } else { + opt.list = this.currentDragObj.default.input.data + } + } + + } + + diffGuestOrHostChangeHandler(num:number) { + // this.currentDiff = num; + console.log('num', num); + console.log('algorithmList', this.algorithmList); + this.algorithmList.forEach(el => { + if (el.moduleName === this.dag.attrForm.moduleName) { + this.currentModuleAttrForm = el.parameters + } + }) + console.log('svgData', this.svgData); + console.log('currentModuleAttrForm', this.currentModuleAttrForm); + + } + getOutputListHandler(output: any) { + if (output.model) { + output.data.forEach((el: string) => { + this.outputRelationships.push({ + value: el, + type: 'data' + }) + }); + output.model.forEach((el: string) => { + this.outputRelationships.push({ + value: el, + type: 'model' + }) + }); + } else { + output.data.forEach((el: string) => { + this.outputRelationships.push({ + value: el, + type: 'data' + }) + }); + } + } + + + // re-render d3 legend + canvasRedraw() { + this.dag.dsl = this.svgData + this.dag.Generate() + this.dag.Draw() + this.program = this.dag.my + this.program.forEach(el => { + if (el.name.indexOf('Evaluation') === -1 && el.name.indexOf('HomoDataSplit') === -1) { + el.value = true + } else { + el.value = false + } + return el + }) + } + + // darg mode zoom + zoom(num: number) { + if (num === -0.1 && String(this.dag.zoomMultiples).slice(0, 3) === '0.1') { + return + } + this.dag.zoomMultiples += num + this.dag.Draw() + } + + // Drag-and-drop mode determines module additions + addAlgorithmHandler(type: string) { + const svgObj: any = { + module: this.currentDragObj.moduleName, + attributes: this.currentDragObj.parameters, + attributeType: type, + conditions: { + input: { + data: [], + model: [] + }, + output: { + data: [], + model: [] + }, + relation: {} + }, + default: { ...this.currentDragObj } + } + if (type !== 'common') { + svgObj.attributeType = 'diff' + svgObj.diffAttribute = this.currentDragObj.diffAttribute + } + this.inputModuleList.forEach(item => { + item.optRelationships.forEach(el => { + const arr = svgObj.conditions.input.data + if (svgObj.conditions.relation.hasOwnProperty(item.inputModule)) { + svgObj.conditions.relation[item.inputModule].list.push({ + key: el.type, + value: el.relation + }) + } else { + svgObj.conditions.relation[item.inputModule] = { + name: item.inputModule, + list: [{ + key: el.type, + value: el.relation + }] + } + } + if (!arr.find((el: string) => el === item.inputModule)) { + arr.push(item.inputModule) + } + }) + }) + this.outputRelationships.forEach(el => { + svgObj.conditions.output.data.push(el.value) + }) + return svgObj + } + // Drag and drop modules to confirm adding or modifying functions + sureOptRelationship() { + this.dag.attrForm.moduleName = '' + this.dag.attrForm.attrList = [] + this.currentModuleAttrForm = {} + let svgObj: any = {} + if (this.svgData.hasOwnProperty(this.dblModuleName)) {// modify + svgObj = this.addAlgorithmHandler(this.currentDragObj.attributeType) + this.svgData[this.dblModuleName] = svgObj + this.dag.attrForm.moduleName = this.dblModuleName + this.canvasRedraw() + for (const key in this.currentDragObj.default.parameters) { + this.dag.attrForm.attrList.push({ + name: key, + value: this.currentDragObj.default.parameters[key], + }) + this.currentModuleAttrForm[key] = this.currentDragObj.default.parameters[key] + } + } else {// new add + svgObj = this.addAlgorithmHandler('common') + this.svgData[this.currentDragObj.moduleName + '_' + this.currentDragObj.count] = svgObj + this.dag.attrForm.moduleName = this.currentDragObj.moduleName + '_' + this.currentDragObj.count + this.algorithmList.forEach((el: any) => { + if (el.moduleName === this.currentDragObj.moduleName) { + el.count++ + } + }); + this.canvasRedraw() + // get attribute list, duplicate corresponding stroage object + for (const key in this.currentDragObj.parameters) { + this.dag.attrForm.attrList.push({ + name: key, + value: this.currentDragObj.parameters[key], + }) + if (Object.prototype.toString.call(this.currentDragObj.parameters[key]) === '[object Object]') { + const result = this.currentDragObj.parameters[key].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[key] = this.currentDragObj.parameters[key] + } else { + this.currentModuleAttrForm[key] = JSON.stringify(this.currentDragObj.parameters[key]) + } + } else if (Object.prototype.toString.call(this.currentDragObj.parameters[key]) === '[object Array]') { + this.currentModuleAttrForm[key] = JSON.stringify(this.currentDragObj.parameters[key]) + } else { + this.currentModuleAttrForm[key] = this.currentDragObj.parameters[key] + } + } + } + this.addModuleFlag = false + this.outputRelationships = [] + this.dag.attrForm.options = 'common' + this.diff = -1 + this.resetHostList() + } + + resetHostList() { + this.hostList.forEach(el => el.form = {}) + } + + // Drag-and-drop mode cancels module addition + cancelOptRelationship() { + this.addModuleFlag = false + this.outputRelationships = [] + } + + // Expand Algorithm Grouping handler + openAlgorithmGroup(i: number) { + this.algorithmDataSourc[i].angle = !this.algorithmDataSourc[i].angle + } + + // Save the properties of the current module to svgData via the form + moduleAttrSave(item: { [key: string]: any }, that: JobNewComponent) { + that.currentModuleAttrForm = {} + if (item.options === 'common') { + item.attrList.forEach((el: any) => { + that.currentModuleAttrForm[el.name] = el.value + }); + } else { + that.resetHostList() + // reset default + that.diff = -1 + item.diffList.forEach((el: any, index: number) => { + if (el.name === 'guest') { + that.currentModuleAttrForm = el.form + } else { + if (index>0) { + that.hostList[index - 1] = { + name: el.name, + form: el.form + } + } else { + that.hostList[index] = { + name: el.name, + form: el.form + } + } + } + }); + // reset default + that.currentDiff = -1 + } + } + + // switch current module property type + changeCommonOrDiffRadio() { + if (this.dag.attrForm.options === 'common') { + if (this.svgData.hasOwnProperty(this.dag.attrForm.moduleName)) { + this.currentModuleAttrForm = this.svgData[this.dag.attrForm.moduleName].attributes + for (const attr in this.svgData[this.dag.attrForm.moduleName].attributes) { + if (Object.prototype.toString.call(this.svgData[this.dag.attrForm.moduleName].attributes[attr]) === '[object Object]' + || Object.prototype.toString.call(this.svgData[this.dag.attrForm.moduleName].attributes[attr]) === '[object Array]') { + const result = this.svgData[this.dag.attrForm.moduleName].attributes[attr].hasOwnProperty('drop_down_box') + if (result) { + this.currentModuleAttrForm[attr] = this.svgData[this.dag.attrForm.moduleName].attributes[attr] + } else { + this.currentModuleAttrForm[attr] = JSON.stringify(this.svgData[this.dag.attrForm.moduleName].attributes[attr]) + } + + } else { + this.currentModuleAttrForm[attr] = this.svgData[this.dag.attrForm.moduleName].attributes[attr] + } + } + } else { + this.dag.attrForm.attrList.forEach((el: any) => { + this.currentModuleAttrForm[el.name] = JSON.parse(JSON.stringify(el)) + }); + } + } else { + this.currentDiff = -1 + } + } + + // Click the module in the d3 legend to save the clicked module attribute to the form + saveCurrentModuleAttr() { + const obj: { [key: string]: any } = {} + const json = JSON.stringify(this.currentModuleAttrForm) + const currentModuleAttrForm = JSON.parse(json) + this.dag.attrForm.attrList.forEach((el: any) => { + obj[el.name] = currentModuleAttrForm[el.name] + }); + + if (this.dag.attrForm.options === 'common') { + for (const key in this.svgData) { + if (key === this.dag.attrForm.moduleName) { + this.svgData[key].attributes = obj + this.svgData[key].attributeType = this.dag.attrForm.options + } + } + } else { // diffrence + if (this.currentDiff === -1) { + for (const key in this.svgData) { + if (key === this.dag.attrForm.moduleName) { + if (this.svgData[this.dag.attrForm.moduleName].diffAttribute) { + const obj2: { [key: string]: any } = this.svgData[this.dag.attrForm.moduleName].diffAttribute['host_0'] + this.svgData[key].diffAttribute = { guest: obj, 'host_0': obj2 } + } else { + this.svgData[key].diffAttribute = { guest: obj, 'host_0': obj } + } + this.dag.attrForm.diffList.push({ + name: 'guest', + form: obj + }) + this.svgData[key].attributeType = this.dag.attrForm.options + } + } + } else {// host + for (const key in this.svgData) { + if (key === this.dag.attrForm.moduleName) { + const str = 'host_' + this.currentDiff + const index = this.hostList.findIndex((el: any) => el.name === str) + if (index !== -1) { + this.hostList.splice(index, 1, { name: str, form: obj }) + } else { + this.hostList.push({ + name: str, + form: obj + }) + } + if (this.svgData[key].diffAttribute) {// first add + this.svgData[key].diffAttribute[str] = obj + this.svgData[key].diffAttribute.guest = this.svgData[key].attributes + this.svgData[key].attributeType = this.dag.attrForm.options + } else { + this.svgData[key].diffAttribute = {} + this.svgData[key].diffAttribute[str] = obj + this.svgData[key].diffAttribute.guest = this.svgData[key].attributes + this.svgData[key].attributeType = this.dag.attrForm.options + } + } + } + } + } + this.dag.dsl = this.svgData + } + + // switch drop copy + switchDropOrCopy(bool: boolean) { + this.dropOrJson = bool + this.program = [] + this.dsl = '' + this.algorithmConfig = '' + + + // Judge reader_0 type according to the number of data config + if(this.displayParticipant.filter(el => el.selected).length > 0) { + this.svgData['reader_0'].attributeType = 'diff' + if (this.dag.attrForm.moduleName === 'reader_0') { + this.dag.attrForm.options = 'diff' + } + } else { + this.svgData['reader_0'].attributeType = 'common' + if (this.dag.attrForm.moduleName === 'reader_0') { + this.dag.attrForm.options = 'common' + } + + } + if (!this.dropOrJson) { + this.svgData = { + reader_0: { + module: "Reader", + attributes: {}, + parameters: {}, + conditions: { + output: { + data: ['data'], + model: [] + } + }, + attributeType: 'common' + } + } + this.canvasRedraw() + } else { + this.dagJson.d3.selectAll("#svg-canvas > *").remove(); + this.algorithmList.forEach(el => { + el.count = 0 + }) + } + } + + // get Algorithm Data Source + getDataSourc() { + this.projectservice.getAlgorithmData().subscribe( + (data: any) => { + const getData = JSON.parse(data.data) + this.algorithmDataSourc = getData.map((el: any) => { + el.angle = true + el.modules.forEach((item: any, i: number) => { + item.count = 0 + for (const key in item.parameters) { + if(Object.prototype.toString.call(item.parameters[key]) === '[object Object]') { + if (item.parameters[key].hasOwnProperty('drop_down_box')) { + item.parameters[key] = { + drop_down_box: item.parameters[key].drop_down_box, + value: item.parameters[key].drop_down_box[0] + } + } + } + } + this.algorithmList.push(item) + }); + return el + }) + } + ) + } + // get dsl conf + getDslConf() { + this.saveNewJob(false); + this.submitGenerated = true; + this.submitGeneratedFailed = this.checkSubmitValid(false); + if (!this.submitGeneratedFailed) { + const data = { + jobDetail: this.jobDetail, + interactive: this.svgData + } + // Determine the number of data config selected to determine the render data structure + if (this.jobDetail.other_site_data.length > 0) { + this.svgData['reader_0'].diffAttribute = { + guest: {} + } + this.jobDetail.other_site_data.forEach((el:any, index:number)=> { + this.svgData['reader_0'].diffAttribute['host_'+index] + }); + } else { + this.svgData['reader_0'].diffAttribute = {} + this.svgData['reader_0'].attributeType = 'common' + } + + + // this.hostList.forEach(el => { + // this.svgData['reader_0'].diffAttribute[el.name] = {} + // }) + + const reqData = JSON.parse(JSON.stringify(this.processingStructure(data))) + for (const key in reqData.interactive) { + for (const key2 in reqData.interactive[key].attributes) { + if (reqData.interactive[key].attributes[key2].hasOwnProperty('drop_down_box')) { + reqData.interactive[key].attributes[key2] = reqData.interactive[key].attributes[key2].value + } + } + } + for (const key in reqData.reqData) { + for (const key3 in reqData.reqData[key].commonAttributes) { + if (reqData.reqData[key].commonAttributes[key3].hasOwnProperty('drop_down_box')) { + reqData.reqData[key].commonAttributes[key3] = reqData.reqData[key].commonAttributes[key3].value + } + } + for (const key2 in reqData.reqData[key].diffAttributes) { + for (const key4 in reqData.reqData[key].diffAttributes[key2]) { + if (reqData.reqData[key].diffAttributes[key2][key4].hasOwnProperty('drop_down_box')) { + reqData.reqData[key].diffAttributes[key2][key4] = reqData.reqData[key].diffAttributes[key2][key4].value + } + } + } + } + // dsl + this.projectservice.getDslAndConf(reqData, 'generateDslFromDag').subscribe( + (data: any) => { + this.dsl = data.data + this.submitGeneratedFailed = false + }, + err => { + this.submitGeneratedFailed = true + } + ) + // conf + this.projectservice.getDslAndConf(reqData, 'generateConfFromDag').subscribe( + (data: any) => { + this.algorithmConfig = data.data + this.submitGeneratedFailed = false + }, + err => { + this.submitGeneratedFailed = true + } + ) + } + } + // Processing data structure + processingStructure(data: any) { + const interactive = JSON.parse(JSON.stringify(data.interactive)) + const reqData: any = {} + for (const key in interactive) { + if (interactive[key].attributeType === 'common') { + for (const attr in interactive[key].attributes) { + interactive[key].attributes[attr] = this.jsonToObj(interactive[key].attributes[attr]) + } + reqData[key] = { + attributeType: interactive[key].attributeType, + commonAttributes: interactive[key].attributes, + diffAttributes: {}, + conditions: { + input: { + data: {} + }, + output: { + data: [], + model: [] + } + }, + module: interactive[key].module + } + if (interactive[key].conditions.input) { + const input = interactive[key].conditions.input.data + const relation = interactive[key].conditions.relation + input.forEach((i: string) => { + for (const reKey in relation) { + if (reKey === i) { + relation[reKey].list.forEach((el: any) => { + reqData[key].conditions.input.data[el.key] = [i + '.' + el.value] + }); + } + } + }); + } + const output = interactive[key].conditions.output + output.data.forEach((el: string) => { + if (el === 'model') { + reqData[key].conditions.output.model.push(el) + } else { + reqData[key].conditions.output.data.push(el) + } + }); + } else { + if (key !== 'reader_0') { + const diffAttribute = interactive[key].diffAttribute + for (const attr in diffAttribute) { + for (const key in diffAttribute[attr]) { + diffAttribute[attr][key] = this.jsonToObj(diffAttribute[attr][key]) + } + } + reqData[key] = { + attributeType: interactive[key].attributeType, + commonAttributes: {}, + diffAttributes: interactive[key].diffAttribute ? interactive[key].diffAttribute : {}, + conditions: { + input: { + data: {} + }, + output: { + data: [], + model: [] + } + }, + module: interactive[key].module + } + if (interactive[key].conditions.input) { + const input = interactive[key].conditions.input.data + const relation = interactive[key].conditions.relation + input.forEach((i: string) => { + for (const reKey in relation) { + if (reKey === i) { + relation[reKey].list.forEach((el: any) => { + reqData[key].conditions.input.data[el.key] = [i + '.' + el.value] + }); + } + } + }); + } + const output = interactive[key].conditions.output + output.data.forEach((el: string) => { + if (el === 'model') { + reqData[key].conditions.output.model.push(el) + } else { + reqData[key].conditions.output.data.push(el) + } + }); + } else { + reqData[key] = { + attributeType: "diff", + commonAttributes: {}, + diffAttributes: interactive[key].diffAttribute, + conditions: { + output: { + data: ["data"] + } + }, + module: "Reader" + } + } + } + } + data.reqData = reqData + return data + } + // JSON string processing + jsonToObj(value: any) { + if (typeof (value) !== 'string') { + return value + } else { + try { + return JSON.parse(value) + } catch (error) { + return value + } + } + + } + // set config + setJsonOrDrag() { + if (this.dropOrJson) { + this.getDslConf() + } else { + if (this.dsl.length > 0 && this.algorithmConfig.length > 0) { + this.startDag(JSON.stringify(this.dslRef.jsonObj), JSON.stringify(this.alRef.jsonObj)) + } + } + } + // interactive validator + interactiveValidator(): boolean { + const keyArr = Object.values(this.svgData) + this.submitGeneratedFailed = false; + this.submitGenerated = false + if (!keyArr.find(el => el.module === 'HomoLR' || el.module === 'HomoSecureboost')) { + this.errorMessage = 'Homolr or homosecureboost module is missing' + this.submitGeneratedFailed = true; + this.submitGenerated = true + return false + } + if (!keyArr.find(el => el.module === 'Evaluation')) { + this.errorMessage = 'Missing evaluation module' + this.submitGeneratedFailed = true; + this.submitGenerated = true + return false + } + return true + } + + //selectionOnChange is triggered when the selection of job type is changed + selectionOnChange(val: any) { + this.resetSelf(); + this.resetParticipant(); + this.resetConfig(); + if (val == "psi") { + this.psi = true; + this.modeling = false; + this.predict = false; + } + if (val == "modeling") { + this.psi = false; + this.modeling = true; + this.predict = false; + } + if (val == "predict") { + this.psi = false; + this.modeling = false; + this.predict = true; + } + if (this.predict) { + this.showModelList(); + } else { + this.hostList = [] + this.displayParticipant = JSON.parse(JSON.stringify(this.participantList)); + const selectList = this.displayParticipant.filter(el => el.selected) + for (let i = 0; i < selectList.length; i++) { + this.hostList.push({ + name: 'host_' + i, + form: {} as any + }) + } + } + } + + //resetSelf is to reset the data selection of current site itself + resetSelf() { + this.self.associated_data = ""; + this.self.data_list = []; + this.self.label_column = ""; + this.displaySelf = JSON.parse(JSON.stringify(this.self)); + this.displayPredictParticipant = []; + } + + //resetParticipant is to reset the data selection of selected participant + resetParticipant() { + this.participantList = []; + for (let participant of this.allParticipantList) { + if (participant.is_current_site) { + this.self.creation_time = participant.creation_time; + this.self.description = participant.description, + this.self.name = participant.name, + this.self.party_id = participant.party_id, + this.self.status = participant.status, + this.self.uuid = participant.uuid + } else { + if (participant.status === 1 || participant.status === 3) { + const party: PartyUser = + { + creation_time: participant.creation_time, + description: participant.description, + name: participant.name, + party_id: participant.party_id, + status: participant.status, + uuid: participant.uuid, + selected: false, + associated_data: "", + data_list: [], + label_column: "" + }; + this.participantList.push(party); + } + } + } + this.hostList = [] + //display the selected participant and data + this.displayParticipant = JSON.parse(JSON.stringify(this.participantList)); + const selectList = this.displayParticipant.filter(el => el.selected) + for (let i = 0; i < selectList.length; i++) { + this.hostList.push({ + name: 'host_' + i, + form: {} as any + }) + } + } + + //resetConfig is to reset all configuration of job, which will be triggered when the selection of job type is changed + resetConfig() { + this.submitGenerated = false; + this.invalidSave = false; + this.submitNewJobFailed = false; + this.submitNewJob = false; + this.submitSaveSelection = false; + this.validationDataPercent = ""; + this.model_name = ""; + this.algorithm = ""; + this.algorithmConfig = ""; + this.dsl = ""; + } + + //showParticipantList is to get all participants + showParticipantList() { + this.projectservice.getParticipantList(this.projectUUID, false) + .subscribe((data: ParticipantListResponse) => { + this.allParticipantList = data.data; + this.initParticipantList(); + }, + err => { + this.isShowParticiapantFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + //initParticipantList is to init the participant list + initParticipantList() { + this.participantList = []; + for (let participant of this.allParticipantList) { + if (participant.is_current_site) { + this.self.creation_time = participant.creation_time; + this.self.description = participant.description, + this.self.name = participant.name, + this.self.party_id = participant.party_id, + this.self.status = participant.status, + this.self.uuid = participant.uuid + } else { + if (participant.status === 1 || participant.status === 3) { + const party: PartyUser = + { + creation_time: participant.creation_time, + description: participant.description, + name: participant.name, + party_id: participant.party_id, + status: participant.status, + uuid: participant.uuid, + selected: false, + associated_data: "", + data_list: [], + label_column: "" + }; + this.participantList.push(party); + } + } + } + this.getSelfAssociatedDataList(); + this.displaySelf = JSON.parse(JSON.stringify(this.self)); + this.hostList = [] + this.displayParticipant = JSON.parse(JSON.stringify(this.participantList)); + const selectList = this.displayParticipant.filter(el => el.selected) + for (let i = 0; i < selectList.length; i++) { + this.hostList.push({ + name: 'host_' + i, + form: {} as any + }) + } + } + //getSelfAssociatedDataList is get current site associated data list + getSelfAssociatedDataList() { + this.projectservice.getParticipantAssociatedDataList(this.projectUUID, this.self.uuid) + .subscribe(data => { + this.selfAssociatedDataListResponse = data; + this.selfdatalist = this.selfAssociatedDataListResponse.data; + }); + } + + //getParticipantAssociatedDataList + getParticipantAssociatedDataList(party: PartyUser, selected: boolean, party_uuid: string) { + this.getParticipantAssociatedDataListIsPending = true; + if (selected) { + this.projectservice.getParticipantAssociatedDataList(this.projectUUID, party_uuid) + .subscribe(data => { + party.data_list = data.data; + this.getParticipantAssociatedDataListIsPending = false; + }, + err => { + this.getParticipantAssociatedDataListIsPending = false; + this.errorMessage = err.error.message; + }); + } + } + + //saveSelection is to save the current data configuration of participant and 'Self' and close the modal + saveSelection() { + this.submitSaveSelection = true; + this.invalidSave = false; + this.checkSelection(this.self, this.participantList); + if (this.invalidSave) { + this.modalErrorMessage = "Please select all required field"; + return; + } + this.displaySelf = JSON.parse(JSON.stringify(this.self)); + this.hostList = [] + this.displayParticipant = JSON.parse(JSON.stringify(this.participantList)); + const selectList = this.displayParticipant.filter(el => el.selected) + for (let i = 0; i < selectList.length; i++) { + this.hostList.push({ + name: 'host_' + i, + form: {} as any + }) + } + this.displayPredictParticipant = JSON.parse(JSON.stringify(this.newPredictParticipantList)); + this.openModal = false; + } + + //resetSelection is to reset the data confiuration of participant to last saved setting + resetSelection(modalStatus: boolean) { + this.self = JSON.parse(JSON.stringify(this.displaySelf)); + this.participantList = JSON.parse(JSON.stringify(this.displayParticipant)); + this.newPredictParticipantList = JSON.parse(JSON.stringify(this.displayPredictParticipant)); + this.openModal = modalStatus; + this.invalidSave = false; + this.submitSaveSelection = false; + } + + //checkSelection is to validate the selection of data confiuration of participants when user trys to save + checkSelection(self: PartyUser, participantList: PartyUser[]) { + if (self.associated_data === "") { + this.invalidSave = true; + } else { + if (this.modeling) { + if (self.label_column === "") this.invalidSave = true; + } + } + if (this.predict) { + for (let party of this.newPredictParticipantList) { + if (party.associated_data === "") this.invalidSave = true; + } + } else { + for (let party of participantList) { + if (party.selected) { + if (party.associated_data === "") this.invalidSave = true; + } + } + } + } + + //checkAssociatedData is to check if there is any associated data availble in current project and alert user if there is not + checkAssociatedData() { + this.openModal = true; + this.showAssociatedDataSubmit = true; + this.projectservice.getAssociatedDataList(this.projectUUID) + .subscribe(data => { + this.noAssociatedData = false; + this.allAssociatedData = data.data; + if (this.allAssociatedData.length === 0) { + this.noAssociatedData = true; + this.showLocalDataListFailed = true; + this.modalErrorMessage = "No associated data available." + } + }, + err => { + this.showLocalDataListFailed = true; + this.modalErrorMessage = err.error.message; + } + ); + } + + //redirectToData is routing to Data management page in current project + redirectToData() { + this.router.navigate(['project-management', 'project-detail', this.projectUUID, 'data']); + } + + //showDataColumn is to get data column of dataset + showDataColumn(val: string) { + let data_id = val.split('+')[1]; + this.dataservice.getDataColumn(data_id) + .subscribe(data => { + this.dataColumnResponse = data; + this.dataColumn = this.dataColumnResponse.data; + }); + } + + //saveNewJob is to update the configuration value and submit the request to create the new job + saveNewJob(submit: boolean) { + //update the configuration + this.jobDetail.name = this.name; + this.jobDetail.description = this.desc; + this.jobDetail.project_uuid = this.projectUUID; + this.jobDetail.initiator_data.data_uuid = this.self.associated_data.split('+')[1]; + //if job type is modeling job + if (this.newJobType == "modeling") { + this.jobDetail.type = 1; + if (this.dslRef && this.alRef) { + this.jobDetail.conf_json = JSON.stringify(this.alRef.jsonObj); + this.jobDetail.dsl_json = JSON.stringify(this.dslRef.jsonObj); + } else { + this.jobDetail.conf_json = this.algorithmConfig; + this.jobDetail.dsl_json = this.dsl; + } + this.jobDetail.training_model_name = this.model_name; + if (this.validationDataPercent === "" || this.validationDataPercent === "0") { + this.validationDataPercent = "0"; + this.jobDetail.training_validation_enabled = false; + } else { + this.jobDetail.training_validation_enabled = true; + } + this.jobDetail.training_validation_percent = Number(this.validationDataPercent); + this.jobDetail.initiator_data.label_name = this.self.label_column; + if (!this.dropOrJson) { + this.jobDetail.evaluate_component_name = "Evaluation_0" + if (this.algorithm === 'al1') { + this.jobDetail.training_algorithm_type = 1; + this.jobDetail.algorithm_component_name = 'HomoLR_0' + } + if (this.algorithm === 'al2') { + this.jobDetail.training_algorithm_type = 2; + this.jobDetail.algorithm_component_name = 'HomoSecureboost_0' + } + } else { + this.jobDetail.training_algorithm_type = this.JobAlgorithmType + for (const data in this.svgData) { + if (data.indexOf('HomoLR') !== -1 || data.indexOf('HomoSecureboost') !== -1) { + this.jobDetail.algorithm_component_name = data + } else if (data.indexOf('Evaluation') !== -1) { + this.jobDetail.evaluate_component_name = data + } + } + } + } else if (this.newJobType == "predict") { + this.jobDetail.type = 2; + this.jobDetail.predicting_model_uuid = this.predictModel; + } else { + this.jobDetail.type = 3; + } + //if job type is predict job + this.jobDetail.other_site_data = []; + if (this.newJobType == "predict") { + for (let party of this.newPredictParticipantList) { + if (party.associated_data != "") { + let selecteddata = { + data_uuid: party.associated_data.split('+')[1], + label_name: "" + } + this.jobDetail.other_site_data.push(selecteddata); + } + } + } else { + for (let party of this.participantList) { + if (party.selected && party.associated_data != "") { + let selecteddata = { + data_uuid: party.associated_data.split('+')[1], + label_name: "" + } + this.jobDetail.other_site_data.push(selecteddata); + } + } + } + this.jobDetail.training_component_list_to_deploy = [] + this.program.forEach(el => { + if (el.value) { + this.jobDetail.training_component_list_to_deploy.push(el.name) + } + }) + //if need to submit the request of creating a new job + if (submit) { + this.submitNewJob = true; + this.submitNewJobFailed = false; + this.submitNewJobFailed = this.checkSubmitValid(submit); + if (!this.submitNewJobFailed) { + this.projectservice.createJob(this.projectUUID, this.jobDetail) + .subscribe(data => { + this.msg.success('serverMessage.create200', 1000) + const new_job_uuid = data.data.uuid; + this.router.navigate(['project-management', 'project-detail', this.projectUUID, 'job', 'job-detail', new_job_uuid]); + }, + err => { + this.submitNewJobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + } + } + + //checkSubmitValid is to validate all configuration before submit the request of creating job + checkSubmitValid(submit: boolean): boolean { + //basic configuration + if (this.jobDetail.name === '') { + this.errorMessage = "Name can not be empty."; + return true; + } + if (this.form.get('name')?.errors?.minlength || this.form.get('name')?.errors?.maxlength) { + this.errorMessage = "Invalid name"; + return true; + } + if (this.jobDetail.initiator_data.data_uuid === '' || this.jobDetail.initiator_data.data_uuid === undefined) { + this.errorMessage = "Please select initiator(self) data."; + return true; + } + //if job type is modeling + if (this.modeling) { + if (this.jobDetail.training_model_name === '') { + this.errorMessage = "Model name can not be empty."; + return true; + } + if (!this.form.get('model_name')?.errors?.minlength || !this.form.get('model_name')?.errors?.maxlength) { + this.errorMessage = this.form.get('model_name')?.errors?.emptyMessage || this.form.get('model_name')?.errors?.message; + } + if ((this.jobDetail.conf_json === '' || this.jobDetail.dsl_json === '') && submit) { + this.errorMessage = "Please generate Algorithm Configuration and Workflow DSL."; + return true; + } + if (this.algorithm === '') { + this.errorMessage = "Please select a algorithm."; + return true; + } + } + //if job type is prediciton + if (this.predict) { + if (this.predictModel === '') { + this.errorMessage = "Please select a model."; + return true; + } + if (this.jobDetail.other_site_data.length != this.newPredictParticipantList) { + this.errorMessage = "Please select participant data."; + return true; + } + } + return false; + } + + // Generate logic diagram in json mode + startDag(dsl: string, al: string) { + this.dagJson = new DagJson(dsl, al, "#svg-canvas", "#component_info"); + this.dagJson.tooltip_css = "dagTip"; + this.dagJson.Generate(); + this.dagJson.Draw(); + this.program = this.dagJson.my + } + + //generateConfig is to get or generate the dsl and algorithm configuration + generateConfig(bool: boolean) { + // Determine which mode is currently + if (bool) {// drag + if (!this.interactiveValidator()) return + this.algorithm = '1' + this.form.get('algorithm')?.setValue('1') + this.setJsonOrDrag() + } else {// json + this.saveNewJob(false); + this.submitGenerated = true; + this.submitGeneratedFailed = this.checkSubmitValid(false); + this.algorithmConfig = '' + this.dsl = '' + if (!this.submitGeneratedFailed) { + this.projectservice.generateJobConfig(this.jobDetail) + .subscribe(data => { + this.algorithmConfig = data.data.conf_json; + this.dsl = data.data.dsl_json; + this.submitGeneratedFailed = false; + this.program = [] + this.startDag(data.data.dsl_json, data.data.conf_json) + }, + err => { + this.submitGeneratedFailed = true; + this.errorMessage = err.error.message; + }); + } + } + } + + //showModelList is to get availble model list for prediction job + showModelList() { + this.projectservice.getModelList(this.projectUUID) + .subscribe((data: any) => { + this.modelList = data.data; + }, + err => { + this.isShowModelFailed = true; + this.errorMessage = err.error.message; + } + ); + } + //onSelectedModelChange is triggered when the model selection of predict job is changed + onSelectedModelChange() { + if (this.predictModel == '') { + this.newPredictParticipantList = []; + this.displayPredictParticipant = JSON.parse(JSON.stringify(this.newPredictParticipantList)); + return; + } + //get new available participant list for current model + this.showPredictParticipant(this.predictModel); + } + + //get available participant list for model + showPredictParticipant(model_uuid: string) { + this.newPredictParticipantList = []; + if (model_uuid === '') return; + this.projectservice.getPredictParticipant(model_uuid) + .subscribe((data: any) => { + this.predictParticipantList = data.data; + //init the participant list for prediction job + for (let party of this.predictParticipantList) { + if (party.site_uuid != this.self.uuid) { + const predictParticipant: PartyUser = { + creation_time: "", + description: "", + name: party.site_name, + party_id: party.site_party_id, + status: 0, + uuid: party.site_uuid, + selected: true, + associated_data: "", + data_list: [], + label_column: "" + }; + this.getParticipantAssociatedDataList(predictParticipant, predictParticipant.selected, predictParticipant.uuid); + this.newPredictParticipantList.push(predictParticipant); + } + } + this.displayPredictParticipant = JSON.parse(JSON.stringify(this.newPredictParticipantList)); + }, + err => { + this.isShowModelFailed = true; + this.errorMessage = err.error.message; + } + ); + } + +} diff --git a/site-portal/frontend/src/app/view/job-new/job-new.css b/site-portal/frontend/src/app/view/job-new/job-new.css new file mode 100644 index 00000000..a662f254 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.css @@ -0,0 +1,96 @@ +.t1{ + width: 250px; + margin-left: -10%; + height:36px; + resize: none; +} +.t2{ + width: 250px; + margin-left: -10%; + height:72px; +} + +select { + width: 200px; +} + +form .radio { + margin-left: 10px; +} +.clr-radio-container { + width: 100%; +} + +.valset{ + width: 30px; + margin-left: -18%; + margin-top: -5%; +} +.cell { + display: inline-block; + position: relative; +} +.selectAlg { + width:250px; + margin-left:-120px; +} +.selectAlg2 { + width:200px; + margin-left:-150px; +} + +.cardtextarea { + width:50%; +} + +.config { + width:50%; + height:200px; +} +input { + width:25%; +} + + + +.generatealert { + padding-left: 0; + padding-top: 0; + padding-bottom: 0; + padding-right: 0; + margin-top:10px; + margin-bottom: 0; + width: 50%; +} +.predict_selector { + width:100%; +} +#svg-canvas { + display: block; + margin: 0 auto; +} + +.model-drag .modal-content .select-item{ + display: flex; +} +.model-drag .modal-content .select-item::ng-deep .clr-form-control{ + margin-top: 0px; +} +.model-drag .modal-content .select-item::ng-deep .clr-form-control .clr-select{ + width: 170px; +} +.model-drag .modal-content::ng-deep .opt { + position: relative; +} +.model-drag .modal-content::ng-deep .opt .trash { + position: absolute; + right: 0px; + top: 50%; + transform: translateY(-50%); +} +.model-drag::ng-deep .plus-circle { + float: right; +} +.info_container::ng-deep .clr-form .clr-form-control{ + margin-top: 0px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/job-new/job-new.module.ts b/site-portal/frontend/src/app/view/job-new/job-new.module.ts new file mode 100644 index 00000000..f2fbf8e9 --- /dev/null +++ b/site-portal/frontend/src/app/view/job-new/job-new.module.ts @@ -0,0 +1,35 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { ClarityModule } from '@clr/angular'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { JobNewRoutingModule } from './job-new-routing' +import { CommonModule } from '@angular/common'; +import { JobNewComponent } from './job-new.component' +import { ShardModule } from 'src/app/shared/shard/shard.module' +import { DragDropModule } from '@angular/cdk/drag-drop'; + +@NgModule({ + declarations: [ + JobNewComponent + ], + imports: [ + CommonModule, + JobNewRoutingModule, + ClarityModule, + FormsModule, + ReactiveFormsModule, + ShardModule, + DragDropModule + ], +}) +export class JobNewModule { } diff --git a/site-portal/frontend/src/app/view/log-in/log-in.component.css b/site-portal/frontend/src/app/view/log-in/log-in.component.css new file mode 100644 index 00000000..e69de29b diff --git a/site-portal/frontend/src/app/view/log-in/log-in.component.html b/site-portal/frontend/src/app/view/log-in/log-in.component.html new file mode 100644 index 00000000..0bbff7a4 --- /dev/null +++ b/site-portal/frontend/src/app/view/log-in/log-in.component.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/log-in/log-in.component.spec.ts b/site-portal/frontend/src/app/view/log-in/log-in.component.spec.ts new file mode 100644 index 00000000..1783956d --- /dev/null +++ b/site-portal/frontend/src/app/view/log-in/log-in.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogInComponent } from './log-in.component'; + +describe('LogInComponent', () => { + let component: LogInComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LogInComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LogInComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/log-in/log-in.component.ts b/site-portal/frontend/src/app/view/log-in/log-in.component.ts new file mode 100644 index 00000000..03e33fc9 --- /dev/null +++ b/site-portal/frontend/src/app/view/log-in/log-in.component.ts @@ -0,0 +1,87 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { AuthService } from 'src/app/service/auth.service'; +import { Router } from '@angular/router'; +import { compile } from '../../../utils/compile' +import { MessageService } from 'src/app/components/message/message.service' +import jwt_decode from "jwt-decode"; + +@Component({ + selector: 'app-log-in', + templateUrl: './log-in.component.html', + styleUrls: ['./log-in.component.css'] +}) + +export class LogInComponent implements OnInit { + // form for login info + form: any = { + username: null, + password: null + }; + username: string = ""; + password: string = ""; + loading = false; + submitted = false; + isLoggedIn = false; + isLoginFailed = false; + errorMessage = ''; + decode: any; + + constructor(private authService: AuthService, private router: Router, private $msg: MessageService) { + } + + ngOnInit(): void { + const redirect = sessionStorage.getItem('sitePortal-redirect') + if (redirect) { + this.$msg.warning('serverMessage.default401') + } + } + // submitLogin is to submit the request to login + submitLogin(): void { + this.submitted = true; + this.loading = true; + const { username, password } = this.form; + this.authService.login(username, password) + .subscribe( + data => { + this.isLoginFailed = false; + this.isLoggedIn = true; + //decode JWT token + var token = data.data; + this.decode = jwt_decode(token); + // store username, id in seesion storage + const encryptName: string = compile(this.decode["name"]); + sessionStorage.setItem('username', encryptName); + sessionStorage.setItem('userId', this.decode["id"]); + // redirect to previous page + const redirect = sessionStorage.getItem('sitePortal-redirect') + if (redirect) { + this.router.navigate([redirect]) + sessionStorage.removeItem('sitePortal-redirect') + } else { + this.router.navigate(['']); + } + try { + this.$msg.close() + } catch (error) { + + } + }, + err => { + this.errorMessage = err.error.message; + this.isLoginFailed = true; + } + ); + } + +} diff --git a/site-portal/frontend/src/app/view/model-detail/model-detail.component.css b/site-portal/frontend/src/app/view/model-detail/model-detail.component.css new file mode 100644 index 00000000..b00510fa --- /dev/null +++ b/site-portal/frontend/src/app/view/model-detail/model-detail.component.css @@ -0,0 +1,47 @@ +.content-area{ + width: 100%; + min-width: 1080px; +} +.clr-input-wrapper { + margin-top: -11%; +} +.content-area .card1 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 200px; + margin-right: 10px; +} +.content-area .card3 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 200px; + margin-right: 10px; +} +.content-area .card2 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 130px; + margin-right: 10px; +} +.evaltable { + width: 30%; +} +clr-datagrid { + margin: 0px; + margin-top: -12px; +} +.card1 { + margin-top: 12px; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +textarea { + resize: none; +} +.t2{ + width: 100%; + height:100px; +} + diff --git a/site-portal/frontend/src/app/view/model-detail/model-detail.component.html b/site-portal/frontend/src/app/view/model-detail/model-detail.component.html new file mode 100644 index 00000000..82067ef1 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-detail/model-detail.component.html @@ -0,0 +1,168 @@ +
+ <<{{'CommonlyUse.back' | translate}} +

{{'modelDetail.modelDetail' | translate}}

+ + +
+
+
+ + +
+
+
    +
  • + {{'CommonlyUse.name' | translate}}: + {{modeldata.name}} +
  • +
  • + {{'modelMg.modelID' | translate}}: + {{modeldata.model_id}} +
  • +
  • + {{'modelDetail.modelVersion' | translate}}: + {{modeldata.model_version}} +
  • +
  • + {{'CommonlyUse.createTime' | translate}}: + {{modeldata.create_time | dateFormatting}} +
  • +
  • + {{'modelDetail.componentName' | translate}}: + {{modeldata.component_name}} +
  • +
+
+
+
+
{{'modelDetail.modelEvaluation' | translate}}:
+
+ + {{'jobDetail.index'|translate}} + {{'modelDetail.metric' | translate}} + + {{keyvalue}} + {{modeldata.evaluation[keyvalue]}} + + +
+
+
+
{{'modelDetail.relatedProjectJobs' | translate}}:
+
+ +
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/model-detail/model-detail.component.spec.ts b/site-portal/frontend/src/app/view/model-detail/model-detail.component.spec.ts new file mode 100644 index 00000000..5a57ef96 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-detail/model-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModelDetailComponent } from './model-detail.component'; + +describe('ModelDetailComponent', () => { + let component: ModelDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ModelDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ModelDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/model-detail/model-detail.component.ts b/site-portal/frontend/src/app/view/model-detail/model-detail.component.ts new file mode 100644 index 00000000..3bbdd379 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-detail/model-detail.component.ts @@ -0,0 +1,198 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ModelService } from '../../service/model.service'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ValidatorGroup } from '../../../config/validators' +import { ClarityIcons, cloudNetworkIcon } from '@cds/core/icon'; +import { constantGather } from '../../../config/constant' + +ClarityIcons.addIcons(cloudNetworkIcon); +export interface ModelDetailResponse { + code: number, + data: {}, + message: "success" +} +export interface ModelDetail { + component_name: string, + create_time: string, + evaluation: any, + job_name: string, + job_uuid: string, + model_id: string, + model_version: string, + name: string, + party_id: number, + project_name: string, + project_uuid: string, + role: string, + uuid: string +} + +@Component({ + selector: 'app-model-detail', + templateUrl: './model-detail.component.html', + styleUrls: ['./model-detail.component.css'] +}) + +export class ModelDetailComponent implements OnInit { + form: FormGroup; + constructor(private route: ActivatedRoute, private modelservice: ModelService, private router: Router, private fb: FormBuilder) { + this.showModelDetail(this.uuid); + this.form = this.fb.group( + ValidatorGroup([ + { + name: 'serviceName', + value: '', + type: ['noSpace'], + max: 20, + min: 2 + }, + { + name: 'type', + type: [''] + }, + { + name: 'parameters_json', + type: ['notRequired', 'json'] + } + ]) + ); + } + + openModal: boolean = false; + openDeleteModal: boolean = false; + serviceName: any; + type: string = ""; + parameters_json: any = ""; + constantGather = constantGather; + ngOnInit(): void { + + } + //uuid is model's uuid + uuid = String(this.route.snapshot.paramMap.get('id')); + modeldata: ModelDetail = { + component_name: "", + create_time: "", + evaluation: {}, + job_name: "", + job_uuid: "", + model_id: "", + model_version: "", + name: "", + party_id: 0, + project_name: "", + project_uuid: "", + role: "", + uuid: "" + }; + errorMessage: any; + isShowModelDetailFailed: boolean = false; + key: any; + isPageLoading: boolean = true; + //showModelDetail is to get the model detail + showModelDetail(uuid: string) { + this.modelservice.getModelDetail(uuid) + .subscribe((data: any) => { + this.modeldata = data.data; + this.key = []; + for (let keyvalue in this.modeldata.evaluation) { + this.key.push(keyvalue); + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isPageLoading = false; + this.isShowModelDetailFailed = true; + } + ); + } + + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + //deleteModel is to submit request to delete Model + deleteModel(uuid: string) { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.modelservice.deleteModel(uuid) + .subscribe(() => { + this.router.navigate(['model-management']); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + //back button + back() { + this.router.navigate(['model-management']); + } + + pendingModeluuid: string = ''; + //openPublishModal is triggerd by 'Publish' button, then open the publich Model modal + openPublishModal(model_uuid: string) { + this.openModal = true; + this.pendingModeluuid = model_uuid; + this.getModelDeployType(model_uuid); + } + + isGetTypeSubmit: boolean = false; + isGetTypeFailed: boolean = false; + modelTypeList: any = []; + //getModelDeployType is to get the supported model deployment type + getModelDeployType(uuid: string) { + this.isGetTypeSubmit = true; + this.isGetTypeFailed = false; + this.modelservice.getModelSupportedDeploymentType(uuid) + .subscribe((data: any) => { + this.modelTypeList = data.data; + }, + err => { + this.isGetTypeFailed = true; + this.errorMessage = err.error.message; + }); + } + + isPublishSubmit: boolean = false; + isPublishFailed: boolean = false; + //publishModel is to send request to publish model + publishModel() { + this.isPublishSubmit = true; + this.isPublishFailed = false; + if (!this.form.valid) { + this.isPublishFailed = true; + this.errorMessage = "Invaild input." + return; + } + if (this.parameters_json === "") this.parameters_json = "{}"; + this.modelservice.publishModel(this.pendingModeluuid, Number(this.type), this.parameters_json, this.serviceName) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isPublishFailed = true; + this.errorMessage = err.error.message; + }); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} + diff --git a/site-portal/frontend/src/app/view/model-mg/model-mg.component.css b/site-portal/frontend/src/app/view/model-mg/model-mg.component.css new file mode 100644 index 00000000..060042ba --- /dev/null +++ b/site-portal/frontend/src/app/view/model-mg/model-mg.component.css @@ -0,0 +1,46 @@ +.content-area{ + width: 100%; + min-width: 1080px; +} +.filter-container { + position: relative; + +} +.searchicon { + size: 10%; +} +.searchinput { + margin: -2%; +} +.searchform{ + margin-left: 80%; +} +.btn-icon{ + width: 20px; + height: 20px; + padding-right: 0; + padding-left: 0; + margin-left: 0; + margin-right: 0; + margin-top: 12%; +} +textarea { + resize: none; +} +.t2{ + width: 100%; + height:100px; +} +.refreshbtn { + float: right; + margin-right: 10px; +} +.cusbtn { + display:flex; + align-items: center; + height: 36px; + position: absolute; + right:24px; +}; + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/model-mg/model-mg.component.html b/site-portal/frontend/src/app/view/model-mg/model-mg.component.html new file mode 100644 index 00000000..041a1766 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-mg/model-mg.component.html @@ -0,0 +1,130 @@ +
+
+

{{'nav.modelMg' | translate}}

+
+
+ +
+ + + + + + + +
+
+ + {{'CommonlyUse.name' | translate}} + {{'modelMg.modelID' | translate}} + {{'modelDetail.modelVersion' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'CommonlyUse.action' | translate}} + + {{model.name}} + {{model.model_id}} + {{model.model_version}} + {{model.create_time | dateFormatting}} + {{'CommonlyUse.delete'| + translate}}   {{'modelMg.publish' + |translate}} + + {{modelList? modelList.length : 0}} {{'CommonlyUse.item' | translate}} + +
+ + + + + + + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/model-mg/model-mg.component.spec.ts b/site-portal/frontend/src/app/view/model-mg/model-mg.component.spec.ts new file mode 100644 index 00000000..71f6e144 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-mg/model-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModelMgComponent } from './model-mg.component'; + +describe('ModelMgComponent', () => { + let component: ModelMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ModelMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ModelMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/model-mg/model-mg.component.ts b/site-portal/frontend/src/app/view/model-mg/model-mg.component.ts new file mode 100644 index 00000000..dcb0c1f2 --- /dev/null +++ b/site-portal/frontend/src/app/view/model-mg/model-mg.component.ts @@ -0,0 +1,203 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import '@cds/core/icon/register.js'; +import { ClarityIcons, searchIcon } from '@cds/core/icon'; +import { ModelService } from '../../service/model.service'; +import { Router } from '@angular/router'; +import { ValidatorGroup } from '../../../config/validators' +import { CustomComparator } from 'src/utils/comparator'; +import { constantGather } from '../../../config/constant' + +ClarityIcons.addIcons(searchIcon); + +export interface ModelElement { + component_name: string, + create_time: string, + job_name: string, + job_uuid: string, + model_id: string, + model_version: string, + name: string, + party_id: number, + project_name: string, + project_uuid: string, + role: string, + uuid: string +} +export interface ModelListResponse { + code: number, + data: [], + message: "success" +} + +@Component({ + selector: 'app-model-mg', + templateUrl: './model-mg.component.html', + styleUrls: ['./model-mg.component.css'] +}) + +export class ModelMgComponent implements OnInit { + timeComparator = new CustomComparator("create_time", "string"); + filterSearchValue: string = ""; + openModal: boolean = false; + serviceName: any; + type: string = ""; + parameters_json: any = ""; + openDeleteModal: boolean = false; + pendingModelId: string = ''; + storageDataList: any[] = [] + constantGather = constantGather + form: FormGroup; + constructor(private fb: FormBuilder, private modelservice: ModelService, private router: Router) { + this.showModelList(); + //form for publishing model + this.form = this.fb.group( + ValidatorGroup([ + { + name: 'serviceName', + value: '', + type: ['noSpace'], + max: 20, + min: 2 + }, + { + name: 'type', + type: [''] + }, + { + name: 'parameters_json', + type: ['notRequired', 'json'] + } + ]) + ); + } + ngOnInit(): void { } + + //openConfirmModal is triggered to open the modal for user to confirm the deletion + openConfirmModal(model_id: string) { + this.pendingModelId = model_id; + this.openDeleteModal = true; + } + + errorMessage: any; + isShowModelFailed: boolean = false; + modelList: any; + isPageLoading: boolean = true; + //showModelList is to get the model list + showModelList() { + this.isPageLoading = true; + this.modelservice.getModelList() + .subscribe((data: ModelListResponse) => { + this.modelList = data.data?.sort((n1: ModelElement, n2: ModelElement) => { return this.timeComparator.compare(n1, n2) }); + this.storageDataList = this.modelList; + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowModelFailed = true; + this.isPageLoading = false; + }); + } + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + //deleteModel is to submit the request for deleting model + deleteModel(uuid: string) { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.modelservice.deleteModel(uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + //filterModelHandle is to process model list with filtered result + filterModelHandle(data: any) { + this.modelList = [] + if (data.searchValue.trim() === '' && data.eligibleList.length < 1) { + this.modelList = this.storageDataList + } else { + data.eligibleList.forEach((el: any) => { + this.modelList.push(el) + }) + } + } + + pendingModeluuid: string = ''; + //openPublishModal is to open the modal for publish model + openPublishModal(model_uuid: string) { + this.openModal = true; + this.pendingModeluuid = model_uuid; + this.getModelDeployType(model_uuid); + } + + isGetTypeSubmit: boolean = false; + isGetTypeFailed: boolean = false; + modelTypeList: any = []; + //getModelDeployType is to get the deployment type of model + getModelDeployType(uuid: string) { + this.isGetTypeSubmit = true; + this.isGetTypeFailed = false; + this.modelservice.getModelSupportedDeploymentType(uuid) + .subscribe((data: any) => { + this.modelTypeList = data.data; + }, + err => { + this.isGetTypeFailed = true; + this.errorMessage = err.error.message; + }); + } + + isPublishSubmit: boolean = false; + isPublishFailed: boolean = false; + //publish model is to send request to publich model + publishModel() { + this.isPublishSubmit = true; + this.isPublishFailed = false; + if (!this.form.valid) { + this.isPublishFailed = true; + this.errorMessage = "Invaild input." + return; + } + if (this.parameters_json === "") this.parameters_json = "{}"; + this.modelservice.publishModel(this.pendingModeluuid, Number(this.type), this.parameters_json, this.serviceName) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isPublishFailed = true; + this.errorMessage = err.error.message; + }); + } + + //refresh button + refresh() { + this.showModelList(); + this.isShowfilter = false; + } + isShowfilter: boolean = false; + showFilter() { + this.isShowfilter = !this.isShowfilter; + } +} diff --git a/site-portal/frontend/src/app/view/project-details/data/data.component.css b/site-portal/frontend/src/app/view/project-details/data/data.component.css new file mode 100644 index 00000000..749024e0 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/data/data.component.css @@ -0,0 +1,14 @@ +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.refreshbtn { + float: right; + margin-right: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/data/data.component.html b/site-portal/frontend/src/app/view/project-details/data/data.component.html new file mode 100644 index 00000000..6723d828 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/data/data.component.html @@ -0,0 +1,71 @@ +
+ + + + + + +
+ + +
+ + {{'CommonlyUse.name' | translate}} + {{'projectDetail.dataProvider' | translate}} + {{'projectDetail.providerPartyID' | translate}} + {{'CommonlyUse.updateTime' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'CommonlyUse.action' | translate}} + + {{data.name}} + {{data.providing_site_name}} + {{data.providing_site_party_id}} + {{data.update_time | dateFormatting}} + {{data.creation_time | dateFormatting}} + {{"projectDetail.cancelAssociation"| translate}} + + + {{associatedDataList ? associatedDataList.length : 0}} {{'CommonlyUse.item' | translate}} + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/data/data.component.spec.ts b/site-portal/frontend/src/app/view/project-details/data/data.component.spec.ts new file mode 100644 index 00000000..9b2c0f6b --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/data/data.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataComponent } from './data.component'; + +describe('DataComponent', () => { + let component: DataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DataComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/data/data.component.ts b/site-portal/frontend/src/app/view/project-details/data/data.component.ts new file mode 100644 index 00000000..45511fdc --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/data/data.component.ts @@ -0,0 +1,184 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CustomComparator } from 'src/utils/comparator'; +import { ProjectService } from '../../../service/project.service'; + +export interface AssociateData { + updated_time: string, + created_time: string, + data_provider: string, + name: string, + associate_status: boolean +} +export interface DataListResponse { + code: number, + data: DataDetail[], + message: "success" +} +export interface DataDetail { + creation_time: string, + data_id: string, + is_local: boolean, + name: string, + providing_site_name: string, + providing_site_party_id: number, + providing_site_uuid: string, + update_time: string +} +export interface LocalData { + creation_time: string, + data_id: string, + name: string, + selected: boolean +} + +@Component({ + selector: 'app-data', + templateUrl: './data.component.html', + styleUrls: ['./data.component.css'] +}) + +export class DataComponent implements OnInit { + options: boolean = false; + openModal: boolean = false; + errorMessage: string = ""; + routeParams = this.route.parent!.snapshot.paramMap; + //uuid is project uuid + uuid = String(this.routeParams.get('id')); + associatedDataListResponse: any; + associatedDataList: any; + + constructor(private route: ActivatedRoute, private projectservice: ProjectService, private router: Router) { + this.showAssociatedDataList(); + } + + ngOnInit(): void { + this.showAssociatedDataList(); + } + + isPageLoading: boolean = true; + showAssociatedDataListFailed: boolean = false; + // showAssociatedDataList is to get associate data list + showAssociatedDataList() { + this.isPageLoading = true; + this.showAssociatedDataListFailed = false; + this.projectservice.getAssociatedDataList(this.uuid) + .subscribe(data => { + this.associatedDataListResponse = data; + this.associatedDataList = this.associatedDataListResponse.data?.sort((n1: any, n2: any) => { return this.createTimeComparator.compare(n1, n2) }); + this.isPageLoading = false; + }, + err => { + this.showAssociatedDataListFailed = true; + this.errorMessage = err.error.message; + this.isPageLoading = false; + }); + } + + localDataList: any; + localDataListResponse: any; + newLocalDataList: LocalData[] = []; + showLocalDataListFailed: boolean = false; + // showlocalDataList + showlocalDataList() { + this.openModal = true; + this.newLocalDataList = []; + this.projectservice.getLocalDataList(this.uuid) + .subscribe(data => { + this.localDataListResponse = data; + this.localDataList = this.localDataListResponse.data; + for (let data of this.localDataList) { + const localdata: LocalData = { + creation_time: "", + data_id: "", + name: "", + selected: false + }; + localdata.creation_time = data.creation_time; + localdata.name = data.name; + localdata.data_id = data.data_id; + this.newLocalDataList.push(localdata); + } + }, + err => { + this.showLocalDataListFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + + associateLocalDataSubmit: boolean = false; + associateLocalDataFailed: boolean = false; + option: any; + noSelected: boolean = true; + //associateLocalData is to associate local data in current project + associateLocalData() { + this.associateLocalDataSubmit = true; + for (let data of this.newLocalDataList) { + if (data.selected) { + this.noSelected = false; + this.projectservice.associateData(this.uuid, data.data_id, data.name) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.associateLocalDataFailed = true; + this.associateLocalDataSubmit = false; + this.errorMessage = err.error.message; + } + ); + } + } + //validation and alert + if (this.noSelected) { + this.errorMessage = "Please select at least one."; + this.associateLocalDataFailed = true; + } + } + + submitDeleteFailed: boolean = false; + deleteDataSubmit: boolean = false; + //deleteAssociatedLocalData is to send the 'cancel association' request of the data the already in the project + deleteAssociatedLocalData(data_uuid: string) { + this.deleteDataSubmit = true; + this.submitDeleteFailed = false; + this.projectservice.deleteAssociatedData(this.uuid, data_uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.submitDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + // reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + // Comparator for data grid + createTimeComparator = new CustomComparator("creation_time", "string"); + updateTimeComparator = new CustomComparator("update_time", "string"); + partyComparator = new CustomComparator("providing_site_name", "string"); + + // refresh button + refresh() { + this.showAssociatedDataList(); + } +} diff --git a/site-portal/frontend/src/app/view/project-details/info/info.component.css b/site-portal/frontend/src/app/view/project-details/info/info.component.css new file mode 100644 index 00000000..5f3742ff --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/info/info.component.css @@ -0,0 +1,29 @@ +.tog{ + display: flex; +} +.tog b { + margin-right: 112px; +} +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.card1 .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 250px; + margin-right: 10px; + line-height: 36px; +} +.infotog{ + margin-top: -15px; +} +.card1{ + min-width: 500px; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} diff --git a/site-portal/frontend/src/app/view/project-details/info/info.component.html b/site-portal/frontend/src/app/view/project-details/info/info.component.html new file mode 100644 index 00000000..a90c8b82 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/info/info.component.html @@ -0,0 +1,85 @@ +
+ +
+
    +
  • + {{'projectDetail.role'| translate}}: + {{'projectDetail.creator'| translate}} +
  • +
  • + {{'projectDetail.role'| translate}}: + {{'projectDetail.participant'| translate}} +
  • +
  • + {{'CommonlyUse.name'| translate}}: + {{projectDetail.name}} +
  • +
  • + {{'CommonlyUse.description'| translate}}: + {{projectDetail.description}} +
  • +
  • + {{'CommonlyUse.createTime'| translate}}: + {{projectDetail.creation_time | dateFormatting}} +
  • +
  • + {{'projectMg.projectManager'| translate}}: + {{projectDetail.manager}} +
  • +
  • + {{'projectMg.managingSiteName'| translate}}: + {{projectDetail.managing_site_name}} +
  • +
  • + {{'projectMg.managingSitePartyID'| translate}}: + {{projectDetail.managing_site_party_id}} +
  • +
  • +

    + {{'projectMg.autoApprovalOfJobs'| translate}}: + +

    +
  • +
+
+
+
+ + + + + + + + + diff --git a/site-portal/frontend/src/app/view/project-details/info/info.component.spec.ts b/site-portal/frontend/src/app/view/project-details/info/info.component.spec.ts new file mode 100644 index 00000000..fead35b4 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/info/info.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InfoComponent } from './info.component'; + +describe('InfoComponent', () => { + let component: InfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ InfoComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/info/info.component.ts b/site-portal/frontend/src/app/view/project-details/info/info.component.ts new file mode 100644 index 00000000..eb4390ee --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/info/info.component.ts @@ -0,0 +1,163 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProjectService } from '../../../service/project.service'; + +export interface ProjectDetailResponse { + code: number, + data: {}, + message: "success" +} +export interface ProjectDetail { + auto_approval_enabled: boolean, + creation_time: string, + description: string, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + uuid: string +} +export interface AutoApprove { + enabled: boolean; +} + +@Component({ + selector: 'app-info', + templateUrl: './info.component.html', + styleUrls: ['./info.component.css'] +}) +export class InfoComponent implements OnInit { + + constructor(private route: ActivatedRoute, private projectservice: ProjectService, private router: Router) { + this.showProjectDetail(); + } + + ngOnInit(): void { + } + options: boolean = false; + routeParams = this.route.parent!.snapshot.paramMap; + //uuid is project uuid + uuid = String(this.routeParams.get('id')); + projectDetailResponse: any; + projectDetail: any = {}; + isPageLoading: boolean = true; + isShowjobInfoFailed: boolean = false; + //showProjectDetail is to get the project detail information + showProjectDetail() { + this.projectservice.getProjectDetail(this.uuid) + .subscribe((data: ProjectDetailResponse) => { + this.projectDetailResponse = data; + this.projectDetail = this.projectDetailResponse.data; + this.options = this.projectDetail.auto_approval_enabled; + this.isPageLoading = false; + }, + err => { + this.isShowjobInfoFailed = true; + this.errorMessage = err.error.message; + this.isPageLoading = false; + }); + } + + autoapprove: AutoApprove = { + enabled: false + }; + errorMessage: string = ""; + isUpdateFailed: boolean = false; + isautoapprovesubmit: boolean = false; + // updateAutoApprove is to update the options to enable auto-approval or not for the jobs in currrent project + updateAutoApprove() { + this.isautoapprovesubmit = true; + this.autoapprove.enabled = this.options; + this.projectservice.putAutoApprove(this.autoapprove, this.uuid) + .subscribe(data => { + }, + err => { + this.errorMessage = err.error.message; + this.isUpdateFailed = true; + }); + + } + + isOpenLeaveModal: boolean = false; + leaveProjectUUID: string = ""; + // openLeaveProjectModal is triggered to confirm the request when user click the 'leave' the project' button (only with the joined project) + openLeaveProjectModal(projectUUID: string) { + this.isOpenLeaveModal = true; + this.leaveProjectUUID = projectUUID; + } + + isLeaveProjectSubmit: boolean = false; + isLeaveProjectFailed: boolean = false; + associatedDataExistWhenLeave: boolean = false; + // leaveProject is to send the leave the project request (only with the joined project) + leaveProject(projectUUID: string) { + this.isLeaveProjectSubmit = true; + this.projectservice.leaveProject(projectUUID) + .subscribe( + data => { + this.router.navigate(['project-management']); + }, + err => { + this.isLeaveProjectFailed = true; + // identity the error reason + if (err.error.message == '') { + this.errorMessage = "Request failed." + } else if (err.error.message === "at least one data association exists, data: site 1 training data") { + this.errorMessage = "" + this.associatedDataExistWhenLeave = true; + } else { + this.errorMessage = err.error.message; + } + } + ); + } + + isOpenCloseModal: boolean = false; + closeProjectUUID: string = ""; + // openCloseProjectModal is triggered and open the confirm modal to send 'close project' request (only with the managed project) + openCloseProjectModal(projectUUID: string) { + this.isOpenCloseModal = true; + this.closeProjectUUID = projectUUID; + } + + isCloseProjectSubmit: boolean = false; + isCloseProjectFailed: boolean = false; + // closeProject is to send the close the project request (only with the managed project) + closeProject(projectUUID: string) { + this.isCloseProjectSubmit = true; + this.projectservice.closeProject(projectUUID) + .subscribe( + data => { + this.router.navigate(['project-management']) + }, + err => { + this.isCloseProjectFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + // redirectToData is to routing to data management in the current project + redirectToData(projectUUID: string) { + this.router.navigate(['project-management', 'project-detail', projectUUID, 'data']); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} diff --git a/site-portal/frontend/src/app/view/project-details/job/job.component.css b/site-portal/frontend/src/app/view/project-details/job/job.component.css new file mode 100644 index 00000000..3979c597 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/job/job.component.css @@ -0,0 +1,14 @@ +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.refreshbtn { + float: right; + margin-right: 12px; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/job/job.component.html b/site-portal/frontend/src/app/view/project-details/job/job.component.html new file mode 100644 index 00000000..074a335b --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/job/job.component.html @@ -0,0 +1,114 @@ +
+ +
+ + + + +
+ + {{'CommonlyUse.name'| translate}} + {{'CommonlyUse.createTime'| translate}} + {{'projectDetail.finishTime'| translate}} + {{'CommonlyUse.id'| translate}} + {{'projectDetail.jobType'| translate}} + {{'projectDetail.initiator'| translate}} + {{'CommonlyUse.status'| translate}} + {{'CommonlyUse.action'| translate}} + + + {{job.name}} + {{job.creation_time | dateFormatting}} + {{job.finish_time | dateFormatting}} + {{'CommonlyUse.null'| translate}} + {{job.uuid}} + + {{constantGather('jobtype', job.type).name | translate}} + + {{job.initiating_site_name}} + + {{constantGather('jobstatus', job.status).name | + translate}} + + + {{'CommonlyUse.delete'| + translate}}  {{'jobDetail.accept'| translate}}  {{'jobDetail.decline'| + translate}} + + + {{jobList ? jobList.length : 0}} {{'CommonlyUse.item' | translate}} + + + + + + + + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/job/job.component.spec.ts b/site-portal/frontend/src/app/view/project-details/job/job.component.spec.ts new file mode 100644 index 00000000..9dc5bd4e --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/job/job.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { JobComponent } from './job.component'; + +describe('JobComponent', () => { + let component: JobComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ JobComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(JobComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/job/job.component.ts b/site-portal/frontend/src/app/view/project-details/job/job.component.ts new file mode 100644 index 00000000..8f03840a --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/job/job.component.ts @@ -0,0 +1,160 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProjectService } from '../../../service/project.service'; +import { JOBSTATUS, JOBTYPE, constantGather } from '../../../../config/constant' +import { CustomComparator } from 'src/utils/comparator'; +@Component({ + selector: 'app-job', + templateUrl: './job.component.html', + styleUrls: ['./job.component.css'] +}) +export class JobComponent implements OnInit { + + constructor(private route: ActivatedRoute, private projectservice: ProjectService, private router: Router) { + this.showJobList(); + } + + jobStatus = JOBSTATUS + jobType = JOBTYPE + constantGather = constantGather + options: boolean = false; + project: any; + openModal: boolean = false; + inviteoption = false; + + ngOnInit(): void { + } + + routeParams = this.route.parent!.snapshot.paramMap; + // uuid is project uuid + uuid = String(this.routeParams.get('id')); + errorMessage: string = ""; + jobList: any; + isShowjobFailed: boolean = false; + isPageLoading: boolean = true; + // showJobList is to get job list in current project + showJobList() { + this.projectservice.getJobList(this.uuid) + .subscribe((data: any) => { + this.jobList = data.data?.sort((n1: any, n2: any) => { return this.jobComparator.compare(n1, n2) }); + this.isPageLoading = false; + }, + err => { + this.isShowjobFailed = true; + this.errorMessage = err.error.message; + this.isPageLoading = false; + } + ); + } + + approveJobFailed: boolean = false; + approveJobsSubmit: boolean = false; + // approve is to approve the job invitation + approve(job_uuid: string) { + this.approveJobsSubmit = true; + this.approveJobFailed = false; + this.projectservice.approveJob(job_uuid) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.approveJobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + refreshJobFailed: boolean = false; + refreshJobsSubmit: boolean = false; + //refresh button + refresh() { + this.refreshJobsSubmit = true; + for (let job of this.jobList) { + this.projectservice.refreshJob(job.uuid) + .subscribe(data => { + }, + err => { + this.refreshJobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + this.reloadCurrentRoute(); + } + + isOpenRejectModal: boolean = false; + reject_job_uuid: string = ""; + rejectErrorMessage: string = ""; + openRejectModal(job_uuid: string) { + this.isOpenRejectModal = true; + this.reject_job_uuid = job_uuid; + } + rejectJobFailed: boolean = false; + rejectJobsSubmit: boolean = false; + // reject is to reject job invitation + reject(job_uuid: string) { + this.rejectJobsSubmit = true; + this.projectservice.rejectJob(job_uuid) + .subscribe(data => { + this.reloadCurrentRoute(); + }, + err => { + this.rejectJobFailed = true; + this.rejectErrorMessage = err.error.message; + if (this.rejectErrorMessage === '') { + this.rejectErrorMessage = "Request failed." + } + } + ); + } + + submitDeleteFailed: boolean = false; + deleteJobSubmit: boolean = false; + deleteerrorMessage: any; + // deleteJob is to send 'delete job' request + deleteJob(job_uuid: string) { + this.deleteJobSubmit = true; + this.projectservice.deleteJob(job_uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.submitDeleteFailed = true; + this.deleteerrorMessage = err.error.message; + }); + } + + pendingJobId: any; + openDeleteModal: boolean = false; + //openConfirmModal is triggered to open the modal for user to confirm the deletion + openConfirmModal(job_id: string) { + this.pendingJobId = job_id; + this.openDeleteModal = true; + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + // Comparator for datagrid + createTimeComparator = new CustomComparator("creation_time", "string"); + statusComparator = new CustomComparator("status", "number"); + typeComparator = new CustomComparator("type", "number"); + partyComparator = new CustomComparator("initiating_site_name", "string"); + jobComparator = new CustomComparator("creation_time", "job"); +} diff --git a/site-portal/frontend/src/app/view/project-details/model/model.component.css b/site-portal/frontend/src/app/view/project-details/model/model.component.css new file mode 100644 index 00000000..45526282 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/model/model.component.css @@ -0,0 +1,21 @@ +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +textarea { + resize: none; +} +.t2{ + width: 100%; + height:100px; +} +.refreshbtn { + float: right; + margin-right: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/model/model.component.html b/site-portal/frontend/src/app/view/project-details/model/model.component.html new file mode 100644 index 00000000..ff3be3bc --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/model/model.component.html @@ -0,0 +1,119 @@ +
+ + + + +
+ + {{'CommonlyUse.name' | translate}} + {{'modelMg.modelID' | translate}} + {{'CommonlyUse.version' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'CommonlyUse.actions' | translate}} + + + {{model.name}} + {{model.model_id}} + {{model.model_version}} + {{model.create_time | dateFormatting}} + {{'CommonlyUse.delete' | + translate}}  {{'modelMg.publish' | translate}} + + {{modelList? modelList.length : 0}} {{'CommonlyUse.item' | translate}} + + + + + + + + + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/model/model.component.spec.ts b/site-portal/frontend/src/app/view/project-details/model/model.component.spec.ts new file mode 100644 index 00000000..ff1730e9 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/model/model.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModelComponent } from './model.component'; + +describe('ModelComponent', () => { + let component: ModelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ModelComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ModelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/model/model.component.ts b/site-portal/frontend/src/app/view/project-details/model/model.component.ts new file mode 100644 index 00000000..84c1abab --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/model/model.component.ts @@ -0,0 +1,198 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CustomComparator } from 'src/utils/comparator'; +import { ModelService } from '../../../service/model.service'; +import { ProjectService } from '../../../service/project.service'; +import { constantGather } from '../../../../config/constant' +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ValidatorGroup } from '../../../../config/validators' + +export interface ProjectModel { + name: string, + version: string, + created_time: string, + last_call_time: string +} +export interface ParticipantListResponse { + code: number, + data: [], + message: "success" +} +export interface ProjectDetailResponse { + code: number, + data: {}, + message: "success" +} +export interface ProjectDetail { + auto_approval_enabled: boolean, + creation_time: string, + description: string, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + uuid: string +} + +@Component({ + selector: 'app-model', + templateUrl: './model.component.html', + styleUrls: ['./model.component.css'] +}) +export class ModelComponent implements OnInit { + + openModal: boolean = false; + serviceName: any; + type: string = ""; + parameters_json: any = ""; + constantGather = constantGather + + form: FormGroup; + constructor(private route: ActivatedRoute, private projectservice: ProjectService, private router: Router, private modelservice: ModelService, private fb: FormBuilder) { + this.showModelList(); + // form for 'publish model' + this.form = this.fb.group( + ValidatorGroup([ + { + name: 'serviceName', + value: '', + type: ['noSpace'], + max: 20, + min: 2 + }, + { + name: 'type', + type: [''] + }, + { + name: 'parameters_json', + type: ['notRequired', 'json'] + } + ]) + ); + } + + ngOnInit(): void { + } + routeParams = this.route.parent!.snapshot.paramMap; + uuid = String(this.routeParams.get('id')); + errorMessage: string = ""; + modelList: any; + isShowModelFailed: boolean = false; + isPageLoading: boolean = true; + g: boolean = true; + // showModelList is to get model list in the current project + showModelList() { + this.isPageLoading = true; + this.projectservice.getModelList(this.uuid) + .subscribe((data: any) => { + this.modelList = data.data?.sort((n1: ProjectModel, n2: ProjectModel) => { return this.createTimeComparator.compare(n1, n2) }); + this.isPageLoading = false; + this.isShowModelFailed = false; + }, + err => { + this.isShowModelFailed = true; + this.errorMessage = err.error.message; + this.isPageLoading = false; + } + ); + } + + // refresh button + refresh() { + this.showModelList(); + } + + openDeleteModal: boolean = false; + pendingModelId: string = ''; + //openConfirmModal is triggered to open the modal for user to confirm the deletion + openConfirmModal(model_id: string) { + this.pendingModelId = model_id; + this.openDeleteModal = true; + } + isDeleteSubmit: boolean = false; + isDeleteFailed: boolean = false; + // deleteModel is to submit the request to 'delete' model + deleteModel(uuid: string) { + this.isDeleteSubmit = true; + this.isDeleteFailed = false; + this.modelservice.deleteModel(uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + pendingModeluuid: string = ''; + // openPublishModal is triggered when + openPublishModal(model_uuid: string) { + this.openModal = true; + this.pendingModeluuid = model_uuid; + this.getModelDeployType(model_uuid); + } + + isGetTypeSubmit: boolean = false; + isGetTypeFailed: boolean = false; + modelTypeList: any = []; + // getModelDeployType is to get the available deployment type of model by model uuid + getModelDeployType(uuid: string) { + this.isGetTypeSubmit = true; + this.isGetTypeFailed = false; + this.modelservice.getModelSupportedDeploymentType(uuid) + .subscribe((data: any) => { + this.modelTypeList = data.data; + }, + err => { + this.isGetTypeFailed = true; + this.errorMessage = err.error.message; + }); + } + + isPublishSubmit: boolean = false; + isPublishFailed: boolean = false; + // publishModel is to request to publish the model + publishModel() { + this.isPublishSubmit = true; + this.isPublishFailed = false; + if (!this.form.valid) { + this.isPublishFailed = true; + this.errorMessage = "Invaild input." + return; + } + if (this.parameters_json === "") this.parameters_json = "{}"; + this.modelservice.publishModel(this.pendingModeluuid, Number(this.type), this.parameters_json, this.serviceName) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.isPublishFailed = true; + this.errorMessage = err.error.message; + }); + } + + // Comparator for datagrid + createTimeComparator = new CustomComparator("create_time", "string"); + +} diff --git a/site-portal/frontend/src/app/view/project-details/participant/participant.component.css b/site-portal/frontend/src/app/view/project-details/participant/participant.component.css new file mode 100644 index 00000000..774d6781 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/participant/participant.component.css @@ -0,0 +1,18 @@ +.toggle{ + margin-left: 350%; + margin-top:-90%; +} +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.refreshbtn { + float: right; + margin-right: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/participant/participant.component.html b/site-portal/frontend/src/app/view/project-details/participant/participant.component.html new file mode 100644 index 00000000..50490cba --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/participant/participant.component.html @@ -0,0 +1,116 @@ +
+ + + + + + +
+ + + +
+ + {{'CommonlyUse.name' | translate}} + {{'site.partyId' | translate}} + {{'CommonlyUse.description' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'projectDetail.role' | translate}} + {{'CommonlyUse.status' | translate}} + {{'CommonlyUse.action' | translate}} + + {{party.name}} + {{party.party_id}} + {{party.description}} + {{party.creation_time|dateFormatting}} + {{'projectDetail.creator' | translate}} + {{'projectDetail.participant' | translate}} + + {{'CommonlyUse.joined' | translate}} + {{'CommonlyUse.pending' | + translate}} + {{'CommonlyUse.joined' | + translate}} + {{'CommonlyUse.delete' | translate}} {{'projectDetail.revokeInvitation' + | translate}} + + {{invitedParticipantList ? invitedParticipantList.length : 0}} {{'CommonlyUse.item' | translate}} + + + \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/participant/participant.component.spec.ts b/site-portal/frontend/src/app/view/project-details/participant/participant.component.spec.ts new file mode 100644 index 00000000..c6086a8b --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/participant/participant.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ParticipantComponent } from './participant.component'; + +describe('ParticipantComponent', () => { + let component: ParticipantComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ParticipantComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ParticipantComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/participant/participant.component.ts b/site-portal/frontend/src/app/view/project-details/participant/participant.component.ts new file mode 100644 index 00000000..72e863fb --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/participant/participant.component.ts @@ -0,0 +1,233 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ProjectService } from '../../../service/project.service'; +import { PARTYSTATUS } from '../../../../config/constant' +import { CustomComparator } from 'src/utils/comparator'; + +export interface Participant { + name: string, + party_id: string, + desc: string, + created_time: string, + status: string +} +export interface PartyUser { + creation_time: string, + description: string, + name: string, + party_id: number, + status: number, + uuid: string, + selected: boolean +} +export interface ParticipantListResponse { + code: number, + data: [], + message: "success" +} +export interface ProjectDetailResponse { + code: number, + data: {}, + message: "success" +} +export interface ProjectDetail { + auto_approval_enabled: boolean, + creation_time: string, + description: string, + managed_by_this_site: boolean, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + uuid: string +} + +@Component({ + selector: 'app-participant', + templateUrl: './participant.component.html', + styleUrls: ['./participant.component.css'] +}) +export class ParticipantComponent implements OnInit { + + constructor(private route: ActivatedRoute, private projectservice: ProjectService, private router: Router) { + this.showProjectDetail(); + this.showInvitedParticipantList(); + } + partyStatus = PARTYSTATUS + options: boolean = false; + project: any; + openModal: boolean = false; + inviteoption = false; + + ngOnInit(): void { + } + + routeParams = this.route.parent!.snapshot.paramMap; + // uuid is project uuid + uuid = String(this.routeParams.get('id')); + participantList: any; + allParticipantList: any; + allParticipantListResponse: any; + newAllParticipantList: PartyUser[] = []; + isShowParticiapantFailed: boolean = false; + invitedParticipantList: any; + isPageLoading: boolean = true; + // showInvitedParticipantList is to get the invited participants list + showInvitedParticipantList() { + this.isPageLoading = true; + this.projectservice.getParticipantList(this.uuid, false) + .subscribe((data: ParticipantListResponse) => { + this.invitedParticipantList = data.data; + this.isPageLoading = false; + }, + err => { + this.isShowParticiapantFailed = true; + this.errorMessage = err.error.message; + this.isPageLoading = false; + } + ); + } + + // showAllParticipantList is to get all the participants list + showAllParticipantList() { + this.openModal = true; + this.newAllParticipantList = []; + this.projectservice.getParticipantList(this.uuid, true) + .subscribe((data: ParticipantListResponse) => { + this.allParticipantListResponse = data; + this.allParticipantList = this.allParticipantListResponse.data; + for (let user of this.allParticipantList) { + const party: PartyUser = + { + creation_time: "", + description: "", + name: "", + party_id: 0, + status: 0, + uuid: "", + selected: false + }; + party.creation_time = user.creation_time; + party.description = user.description; + party.name = user.name; + party.party_id = user.party_id; + party.status = user.status; + party.uuid = user.uuid; + if (user.status === this.partyStatus.Owner || user.status === this.partyStatus.Pending || user.status === this.partyStatus.Joined) { + party.selected = true; + } + + this.newAllParticipantList.push(party); + } + }, + err => { + this.isShowParticiapantFailed = true; + this.errorMessage = err.error.message; + }); + } + + isSubmitInvitation: boolean = false; + isSubmitInvitationFailed: boolean = false; + noSelected: boolean = true; + // submitInvitation is to submit the request to invite participant + submitInvitation() { + this.isSubmitInvitation = true; + this.isSubmitInvitationFailed = false; + for (let party of this.newAllParticipantList) { + if (party.status == this.partyStatus.Unknown && party.selected) { + if (this.noSelected) this.noSelected = false; + this.projectservice.postInvitation(this.uuid, party.description, party.name, party.party_id, party.uuid) + .subscribe( + data => { + this.reloadCurrentRoute(); + }, + err => { + this.isSubmitInvitationFailed = true; + this.isSubmitInvitation = false; + this.errorMessage = err.error.message; + } + ); + } + } + if (this.noSelected) { + this.errorMessage = "Please select."; + this.isSubmitInvitationFailed = true; + } + } + + projectDetailResponse: any; + projectDetail: ProjectDetail = { + auto_approval_enabled: false, + creation_time: "", + description: "", + managed_by_this_site: false, + manager: "", + managing_site_name: "", + managing_site_party_id: 0, + name: "", + uuid: "" + } + // showProjectDetail is to get the preject detail to get if the auto approval is enabled + showProjectDetail() { + this.projectservice.getProjectDetail(this.uuid) + .subscribe((data: ProjectDetailResponse) => { + this.projectDetailResponse = data; + this.projectDetail = this.projectDetailResponse.data; + this.options = this.projectDetail.auto_approval_enabled; + }); + } + + errorMessage: string = ""; + isDeleteSubmitted: boolean = false; + submitDeleteFailed: boolean = false; + // deleteParticipant is to request to remove particiapnt (only with the project is managed by current site) + deleteParticipant(party_uuid: string) { + this.isDeleteSubmitted= true; + this.projectservice.deleteParticipant(this.uuid, party_uuid) + .subscribe(() => { + this.reloadCurrentRoute(); + }, + err => { + this.submitDeleteFailed = true; + this.errorMessage = err.error.message; + }); + } + + cur_party_uuid: string = ""; + isOpenAlertModal: boolean = false; + // openAlertModal is triggered to open the confirm modal when user want to 'delete Participant' + openAlertModal(party_uuid: string) { + this.isOpenAlertModal = true; + this.cur_party_uuid = party_uuid; + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + // refresh button + refresh() { + this.showProjectDetail(); + this.showInvitedParticipantList(); + } + + // comparator for datagrid + createTimeComparator = new CustomComparator("creation_time", "string"); + roleComparator = new CustomComparator("status", "number"); + statusComparator = new CustomComparator("status", "number"); +} diff --git a/site-portal/frontend/src/app/view/project-details/project-detail.module.ts b/site-portal/frontend/src/app/view/project-details/project-detail.module.ts new file mode 100644 index 00000000..edd6d0c1 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-detail.module.ts @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { ClarityModule } from '@clr/angular'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ProjectDetailsRoutingModule, declarations } from './project-details-routing' +import { CommonModule } from '@angular/common'; +import { ShardModule } from 'src/app/shared/shard/shard.module' + +@NgModule({ + declarations: [ + ...declarations + ], + imports: [ + ProjectDetailsRoutingModule, + ClarityModule, + FormsModule, + CommonModule, + ShardModule, + ReactiveFormsModule + ], +}) +export class ProjectDetailModule {} diff --git a/site-portal/frontend/src/app/view/project-details/project-details-routing.ts b/site-portal/frontend/src/app/view/project-details/project-details-routing.ts new file mode 100644 index 00000000..1049b46e --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-details-routing.ts @@ -0,0 +1,48 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ProjectDetailsComponent } from './project-details.component'; +import { InfoComponent } from './info/info.component' +import { DataComponent } from './data/data.component'; +import { JobComponent } from './job/job.component'; +import { ModelComponent } from './model/model.component'; +import { ParticipantComponent } from './participant/participant.component'; + + +const routes: Routes = [ + { + path:'', + component: ProjectDetailsComponent, + children: [ + { path: '', redirectTo: 'info', pathMatch: 'full' }, + { path:'info', component: InfoComponent}, + { path:'job', component: JobComponent}, + { path:'data', component: DataComponent}, + { path:'model', component: ModelComponent}, + { path:'participant', component: ParticipantComponent} + ] + } +] +export const declarations = [ + ProjectDetailsComponent, + InfoComponent, + JobComponent, + DataComponent, + ModelComponent, + ParticipantComponent +] +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ProjectDetailsRoutingModule { } \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/project-details.component.css b/site-portal/frontend/src/app/view/project-details/project-details.component.css new file mode 100644 index 00000000..8a0874d8 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-details.component.css @@ -0,0 +1,20 @@ +.content-area { + width: 100%; + min-width: 1080px; +} +exact.toggle{ + margin-left: 350%; + margin-top:-90%; +} +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +.btn.my-btn.btn-link.nav-link { + box-shadow: none; +} +.btn.my-btn.btn-link.nav-link.my-active { + box-shadow: 0 -0.15rem 0 #0072a3 inset; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/project-details.component.html b/site-portal/frontend/src/app/view/project-details/project-details.component.html new file mode 100644 index 00000000..b070277a --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-details.component.html @@ -0,0 +1,47 @@ +
+ <<{{'CommonlyUse.back' | translate}} +

{{'projectDetail.projectDetail' | translate}}

+
+ +
+ + + + + + + + + + + + + + + + + +
+ +
+
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-details/project-details.component.spec.ts b/site-portal/frontend/src/app/view/project-details/project-details.component.spec.ts new file mode 100644 index 00000000..2c879bde --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectDetailsComponent } from './project-details.component'; + +describe('ProjectDetailsComponent', () => { + let component: ProjectDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProjectDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-details/project-details.component.ts b/site-portal/frontend/src/app/view/project-details/project-details.component.ts new file mode 100644 index 00000000..73a235ba --- /dev/null +++ b/site-portal/frontend/src/app/view/project-details/project-details.component.ts @@ -0,0 +1,165 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Location } from '@angular/common' +import { ProjectService } from '../../service/project.service'; + +export interface Job { + name: string, + time: string, + id: string, + type: string, + intiator: string, + status: string, + job_invited: boolean, +} +export interface AssociateData { + updated_time: string, + created_time: string, + data_provider: string, + name: string, + associate_status: boolean +} +export interface Participant { + name: string, + party_id: string, + desc: string, + created_time: string, + status: string +} +export interface PartyUser { + creation_time: string, + description: string, + name: string, + party_id: number, + status: number, + uuid: string, + selected: boolean +} +export interface ParticipantListResponse { + code: number, + data: [], + message: "success" +} +export interface ProjectDetailResponse { + code: number, + data: {}, + message: "success" +} +export interface ProjectDetail { + auto_approval_enabled: boolean, + creation_time: string, + description: string, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + uuid: string +} +export interface DataListResponse { + code: number, + data: DataDetail[], + message: "success" +} +export interface DataDetail { + creation_time: string, + data_id: string, + is_local: boolean, + name: string, + providing_site_name: string, + providing_site_party_id: number, + providing_site_uuid: string, + update_time: string +} +export interface LocalData { + creation_time: string, + data_id: string, + name: string, + selected: boolean +} +export interface AutoApprove { + enabled: boolean; +} + +@Component({ + selector: 'app-project-details', + templateUrl: './project-details.component.html', + styleUrls: ['./project-details.component.css'] +}) + +export class ProjectDetailsComponent implements OnInit { + + constructor(public route: ActivatedRoute, public localtion: Location, public router: Router, private projectservice: ProjectService) { + this.showProjectDetail(); + this.showJobList(); + } + + options: boolean = false; + project: any; + openModal: boolean = false; + inviteoption = false; + back() { + this.router.navigate(['project-management']); + } + ngOnInit(): void { + } + + routeParams = this.route.parent!.snapshot.paramMap; + // uuid is project uuid + uuid = String(this.routeParams.get('id')); + errorMessage: string = ""; + isShowjobInfoFailed: boolean = false; + //showProjectDetail is to get the project detail information + showProjectDetail() { + this.isShowjobInfoFailed = false; + this.projectservice.getProjectDetail(this.uuid) + .subscribe((data: any) => { + }, + err => { + this.isShowjobInfoFailed = true; + this.errorMessage = err.error.message; + }); + } + + pendingJobList: any; + isShowjobFailed: boolean = false; + // showJobList is to get the current job list in the current project, then filted the job invitation for alerting user + showJobList() { + this.pendingJobList = []; + this.projectservice.getJobList(this.uuid) + .subscribe((data: any) => { + for (let pendingjob of data.data) { + if (pendingjob.pending_on_this_site) { + this.pendingJobList.push(pendingjob); + } + } + }, + err => { + this.isShowjobFailed = true; + this.errorMessage = err.error.message; + } + ); + } + // refresh button + refresh() { + this.reloadCurrentRoute(); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } +} diff --git a/site-portal/frontend/src/app/view/project-mg/project-mg.component.css b/site-portal/frontend/src/app/view/project-mg/project-mg.component.css new file mode 100644 index 00000000..b425d3df --- /dev/null +++ b/site-portal/frontend/src/app/view/project-mg/project-mg.component.css @@ -0,0 +1,69 @@ +.content-area{ + width: 100%; +} +.btn3 { + margin-left: 80%; + margin:88*16; + display: flex; + margin-top:-10%; +} +.t1{ + width: 98%; +} +.t2{ + width: 100%; + height:100px; +} +.t3{ + width: 70%; + line-height: 50%; + display: flex; +} +.toggle{ + margin-left: 40%; + margin-top:-22px; +} +.tabbtn{ + font-size: 120%; +} +.card-text{ + text-align: left; + margin-top:-3%; +} +.ctt{ + text-align: left; + line-height: 13px; +} +.alert{ + padding-left: 0%; + padding-right: 0%; + padding-top: 0%; + padding-bottom: 0%; +} +textarea { + resize: none; +} + +.verticaltab /deep/ .nav{ + width:100px; + margin:0; +} +.verticaltab /deep/ section{ + width: 100%; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.card .card-block .card-text .list span:first-of-type { + font-size: 15px; + display: inline-block; + width: 150px; + margin-right: 12px; + line-height: 36px; +} +.refreshbtn { + float: right; + margin-right: 12px; +} + diff --git a/site-portal/frontend/src/app/view/project-mg/project-mg.component.html b/site-portal/frontend/src/app/view/project-mg/project-mg.component.html new file mode 100644 index 00000000..7b114d61 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-mg/project-mg.component.html @@ -0,0 +1,305 @@ +
+
+

{{'nav.projectMg'| translate}}

+
+
+ +
+ + + + + + +
+ + + + + +
+ {{'projectMg.allProject' | translate}}{{joinedProjectList ? joinedProjectList.length : 0}} + {{'projectMg.ownedProject' | translate}}{{myProjectList.length}} + {{'projectMg.joinedProject' | translate}}{{othersProjectList.length}} + {{'projectMg.closedProject' | translate}}{{closedProjectList.length}} + + + + + + + {{errorMessage}} + + + + +
+ + {{'projectMg.projectName' | translate}} + {{'CommonlyUse.description' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'projectMg.projectManager' | translate}} + {{'projectMg.managingSiteName' | translate}} + {{'projectMg.managingSitePartyID' | translate}} + {{'projectMg.paticipant' | translate}} + {{'projectMg.job' | translate}} + {{'CommonlyUse.action' | translate}} + + {{project.name}} + + + {{project.description}} + {{project.creation_time | dateFormatting}} + {{project.manager}} + {{project.managing_site_name}} + {{project.managing_site_party_id}} + {{project.participants_num}} + {{'Running' | translate}}: {{project.running_job_num}}
{{'Succeeded' | translate}}: + {{project.success_job_num}}
+ {{'projectMg.close' | translate}} + {{'projectMg.leave' | translate}} +
+ {{joinedProjectList ? joinedProjectList.length : 0}} {{'CommonlyUse.item' | translate}} + +
+
+ +
+ + {{'projectMg.projectName' | translate}} + {{'CommonlyUse.description' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'projectMg.projectManager' | translate}} + {{'projectMg.paticipant' | translate}} + {{'projectMg.job' | translate}} + {{'CommonlyUse.action' | translate}} + + {{project.name}} + + + {{project.description}} + {{project.creation_time | dateFormatting}} + {{project.manager}} + {{project.participants_num}} + {{'Running' | translate}}: {{project.running_job_num}}
{{'Succeeded' | translate}}: + {{project.success_job_num}}
+ {{'projectMg.close' | translate}} +
+ {{myProjectList.length}} {{'CommonlyUse.item' | translate}} +
+
+ +
+ + {{'projectMg.projectName' | translate}} + {{'CommonlyUse.description' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'projectMg.projectManager' | translate}} + {{'projectMg.managingSiteName' | translate}} + + {{'projectMg.managingSitePartyID' | translate}} + {{'projectMg.paticipant' | translate}} + {{'projectMg.job' | translate}} + {{'CommonlyUse.action' | translate}} + + {{project.name}} + + + {{project.description}} + {{project.creation_time | dateFormatting}} + {{project.manager}} + {{project.managing_site_name}} + {{project.managing_site_party_id}} + {{project.participants_num}} + {{'Running' | translate}}: {{project.running_job_num}}
{{'Succeeded' | translate}}: + {{project.success_job_num}}
+ {{'projectMg.leave' | translate}} +
+ {{othersProjectList.length}} {{'CommonlyUse.item' | translate}} +
+
+ +
+ + {{'projectMg.projectName' | translate}} + {{'CommonlyUse.description' | translate}} + {{'CommonlyUse.createTime' | translate}} + {{'projectMg.projectManager' | translate}} + {{'projectMg.managingSiteName' | translate}} + + {{'projectMg.managingSitePartyID' | translate}} + {{'CommonlyUse.status' | translate}} + + {{project.name}} + {{project.description}} + {{project.creation_time | dateFormatting}} + {{project.manager}} + {{project.managing_site_name}} + {{project.managing_site_party_id}} + {{project.closing_status}} + + {{closedProjectList.length}} {{'CommonlyUse.item' | translate}} + +
+
+
+ + + + +
+
+
+
+ {{project.name}} +
+
+
+
    +
  • + {{'CommonlyUse.description' | translate}}: + {{project.description}} +
  • +
  • + {{'CommonlyUse.createTime' | translate}}: + {{project.creation_time | dateFormatting}} +
  • +
  • + {{'projectMg.projectManager' | translate}}: + {{project.manager}} +
  • +
  • + {{'CommonlyUse.siteName' | translate}}: + {{project.managing_site_name}} +
  • +
  • + {{'site.partyId'| translate}}: + {{project.managing_site_party_id}} +
  • +
+
+
+ +
+
+
+
+
+
+ + + + + + + + +
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/project-mg/project-mg.component.spec.ts b/site-portal/frontend/src/app/view/project-mg/project-mg.component.spec.ts new file mode 100644 index 00000000..329e1c95 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-mg/project-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectMgComponent } from './project-mg.component'; + +describe('ProjectMgComponent', () => { + let component: ProjectMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProjectMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/project-mg/project-mg.component.ts b/site-portal/frontend/src/app/view/project-mg/project-mg.component.ts new file mode 100644 index 00000000..fcea51b6 --- /dev/null +++ b/site-portal/frontend/src/app/view/project-mg/project-mg.component.ts @@ -0,0 +1,328 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy } from '@angular/core'; +import '@cds/core/icon/register.js'; +import { ClarityIcons, newIcon, refreshIcon, tasksIcon, windowCloseIcon } from '@cds/core/icon'; +import { ProjectService } from '../../service/project.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder } from '@angular/forms'; +import { ValidatorGroup } from '../../../config/validators' +import { MessageService } from '../../components/message/message.service' +import { CustomComparator } from 'src/utils/comparator'; +import { SiteService } from 'src/app/service/site.service'; + +ClarityIcons.addIcons(windowCloseIcon, newIcon, tasksIcon, refreshIcon); + +export interface JoinedProject { + creation_time: string, + local_data_num: number, + managed_by_this_site: boolean, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + participants_num: number, + pending_job_exist: boolean, + remote_data_num: number, + running_job_num: number, + success_job_num: number, + uuid: string +} +export interface InvitedProject { + creation_time: string, + manager: string, + managing_site_name: string, + managing_site_party_id: number, + name: string, + uuid: string +} +export interface ProjectListResponse { + code: number, + data: { + invited_projects: InvitedProject[], + joined_projects: JoinedProject[] + }, + message: "success" +} +export interface NewProject { + auto_approval_enabled: boolean, + description: string, + name: string +} + +@Component({ + selector: 'app-project-mg', + templateUrl: './project-mg.component.html', + styleUrls: ['./project-mg.component.css'] +}) + +export class ProjectMgComponent implements OnInit, OnDestroy { + + constructor(private projectservice: ProjectService, private route: ActivatedRoute, + private router: Router, private fb: FormBuilder, private msg: MessageService, private siteService: SiteService) { + this.showProjectList(); + } + + ngOnInit(): void { + } + ngOnDestroy(): void { + this.msg.close() + } + + openModal: boolean = false; + projName: string = ''; + desc: string = ''; + options: any; + selectOptions: string = "all"; + projectListResponse: any; + projectlist: any = []; + joinedProjectList: any = []; + invitedProjectList: any = []; + myProjectList: JoinedProject[] = []; + othersProjectList: JoinedProject[] = []; + closedProjectList: JoinedProject[] = []; + myPendingJobExist: boolean = false; + othersPendingJobExist: boolean = false; + + // form is for 'create form' project + form = this.fb.group( + ValidatorGroup([ + { + name: 'projName', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'desc', + type: [''] + }, + { + name: 'options', + type: [''] + }, + ]) + ); + + isShowProjectFailed: boolean = false; + isPageLoading: boolean = true; + // showProjectList is to get all project list and filter with type + showProjectList() { + this.isPageLoading = true; + this.myProjectList = []; + this.othersProjectList = []; + this.closedProjectList = []; + this.projectservice.getProjectList() + .subscribe((data: ProjectListResponse) => { + this.projectListResponse = data; + this.projectlist = this.projectListResponse.data; + this.joinedProjectList = this.projectlist.joined_projects; + this.invitedProjectList = this.projectlist.invited_projects; + this.closedProjectList = this.projectlist.closed_projects? this.projectlist.closed_projects : []; + for (let project of this.joinedProjectList) { + if (project.managed_by_this_site) { + this.myProjectList.push(project); + if (project.pending_job_exist && !this.myPendingJobExist) this.myPendingJobExist = true; + } else { + this.othersProjectList.push(project); + if (project.pending_job_exist && !this.othersPendingJobExist) this.othersPendingJobExist = true; + } + } + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isShowProjectFailed = true; + this.isPageLoading = false; + }); + } + + isCreateSubmitted: boolean = false; + isCreatedFailed: boolean = false; + errorMessage: string = ""; + // createNewProject is to request for creating new project + createNewProject() { + this.isCreateSubmitted = true; + this.isCreatedFailed = false; + // validation before submit the request + if (this.projName === '') { + this.isCreatedFailed = true; + this.errorMessage = 'Name can not be empty'; + return; + } + if (!this.form.valid) { + this.errorMessage = 'Invalid information.'; + this.isCreatedFailed = true; + return; + } + this.projectservice.createProject(this.options, this.desc, this.projName) + .subscribe( + data => { + this.msg.success('serverMessage.create200', 1000) + this.isCreatedFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isCreatedFailed = true; + } + ); + + } + + // acceptInvitation is to accept the project invitation from other party + acceptProjectInvitationFailed: boolean = false; + acceptProjectInvitationSubmit: boolean = false; + acceptInvitation(projectUUID: string) { + this.acceptProjectInvitationFailed = false + this.acceptProjectInvitationSubmit = true + this.projectservice.acceptInvitation(projectUUID) + .subscribe( + data => { + this.reloadCurrentRoute() + }, + err => { + this.acceptProjectInvitationFailed = true + this.errorMessage = err.error.message + } + ); + } + + isOpenRejectModal: boolean = false; + rejectProjectUUID: string = ""; + rejectErrorMessage: string = ""; + // openRejectModal is trigger to open the confirmation modal when user try to 'reject project invitation' + openRejectModal(projectUUID: string) { + this.isOpenRejectModal = true; + this.rejectProjectUUID = projectUUID; + } + + rejectProjectInvitationFailed: boolean = false; + rejectProjectInvitationSubmit: boolean = false; + // rejectInvitation is to request for 'reject project invitation' + rejectInvitation(projectUUID: string) { + this.rejectProjectInvitationSubmit = true; + this.rejectProjectInvitationFailed = false; + this.projectservice.rejectInvitation(projectUUID) + .subscribe( + data => { + this.reloadCurrentRoute(); + }, + err => { + this.rejectProjectInvitationFailed = true; + this.errorMessage = err.error.message; + if (this.rejectErrorMessage === '') { + this.rejectErrorMessage = "Request failed." + } + } + ); + } + + // closeModal is to close the modal of 'creat project' + closeModal() { + this.openModal = false; + this.isCreateSubmitted = false; + this.isCreatedFailed = false; + } + + //newProjectAuthentication is to send request to get current user for authentication in case the session is expired + newProjectAuthentication() { + this.openModal = true + this.siteService.getCurrentUser().subscribe( + data => { }, + err => { } + ) + } + + isOpenLeaveModal: boolean = false; + leaveProjectUUID: string = ""; + // openLeaveProjectModal is trigger to open the confirmation modal when user try to 'leave project' (only with joined project) + openLeaveProjectModal(projectUUID: string) { + this.isOpenLeaveModal = true; + this.leaveProjectUUID = projectUUID; + } + + isLeaveProjectSubmit: boolean = false; + isLeaveProjectFailed: boolean = false; + associatedDataExistWhenLeave: boolean = false; + // leaveProject is to send 'leave project' request (only with joined project) + leaveProject(projectUUID: string) { + this.isLeaveProjectSubmit = true; + this.projectservice.leaveProject(projectUUID) + .subscribe( + data => { + this.reloadCurrentRoute(); + }, + err => { + this.isLeaveProjectFailed = true; + if (err.error.message == '') { + this.errorMessage = "Request failed." + } else if (err.error.message === "at least one data association exists, data: site 1 training data") { + this.errorMessage = "" + this.associatedDataExistWhenLeave = true; + } else { + this.errorMessage = err.error.message; + } + } + ); + } + + //redirectToData is routing to Data management page in current project + redirectToData(projectUUID: string) { + this.router.navigate(['project-management', 'project-detail', projectUUID, 'data']); + } + + isOpenCloseModal: boolean = false; + closeProjectUUID: string = ""; + // openCloseProjectModal is trigger to open the confirmation modal when user try to 'close project' (only with managed project) + openCloseProjectModal(projectUUID: string) { + this.isOpenCloseModal = true; + this.closeProjectUUID = projectUUID; + } + + isCloseProjectSubmit: boolean = false; + isCloseProjectFailed: boolean = false; + // closeProject is to request for closing project (only with managed project) + closeProject(projectUUID: string) { + this.isCloseProjectSubmit = true; + this.projectservice.closeProject(projectUUID) + .subscribe( + data => { + this.reloadCurrentRoute(); + }, + err => { + this.isCloseProjectFailed = true; + this.errorMessage = err.error.message; + } + ); + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/user-management', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + // refresh button + refresh() { + this.showProjectList(); + } + + // comparator for datagrid + timeComparator = new CustomComparator("creation_time", "string"); + participantComparator = new CustomComparator("participants_num", "number"); + managingSiteComparator = new CustomComparator("managing_site_name", "string"); +} + diff --git a/site-portal/frontend/src/app/view/site-config/site-config.component.css b/site-portal/frontend/src/app/view/site-config/site-config.component.css new file mode 100644 index 00000000..8f7a2463 --- /dev/null +++ b/site-portal/frontend/src/app/view/site-config/site-config.component.css @@ -0,0 +1,79 @@ +.content-area{ + width: 100%; + min-width: 1080px; +} +.alert { + padding-left: 0; + padding-top: 0; + padding-bottom: 0; + padding-right: 0; + margin-top:10px; + margin-bottom: 0; + width: 50%; +} +.check_circle{ + width:23px; + height: 23px; +} +.btn1 { + padding-right: 2px; +} +.t1 { + width: 70%; + height: 60px; +} +.t2 { + width: 400px; + height: 100px; +} +.kubeinput { + width: 400px; +} +.kubepwdinput { + width: 365px; +} + +.modalalert { + width: 100%; + padding: 10px; +} +.refreshbtn { + float: right; + margin-right: 10px; +} +clr-accordion { + min-width: 1600px; +} +.site_address{ + display: flex; +} +.site_address select { + flex: 1; + margin-top: 1px; +} +.site_address clr-select-container { + min-width: 400px; +} +.site_address clr-input-container { + margin-left: -230px; +} +.fml_manager_endpoint { + display: flex; +} +.fml_manager_endpoint select { + flex: 1; + margin-top: 1px; +} +.fml_manager_endpoint clr-select-container { + min-width: 400px; +} +.fml_manager_endpoint clr-input-container { + margin-left: -640px; +} +.server_name_container input { + width:265px; + margin-left: -36px; +} +.server_name_container clr-control-helper { + margin-left: -36px; +} diff --git a/site-portal/frontend/src/app/view/site-config/site-config.component.html b/site-portal/frontend/src/app/view/site-config/site-config.component.html new file mode 100644 index 00000000..a6b635ee --- /dev/null +++ b/site-portal/frontend/src/app/view/site-config/site-config.component.html @@ -0,0 +1,283 @@ +
+
+ + + + + +

{{'nav.siteConfiguration' | translate}}

+
+
+ + + {{'nav.siteInfo' | translate}} + + +
+ + + + + {{form.get('name')?.errors?.emptyMessage || form.get('name')?.errors?.message | + translate}} + {{'CommonlyUse.few' | + translate}}{{form.get('name')?.errors?.minlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + {{'CommonlyUse.many' | + translate}}{{form.get('name')?.errors?.maxlength.requiredLength}}{{'CommonlyUse.character' + | translate}} + +
+ + + + + + + {{ form.get('site_ip')?.errors?.message | translate}} + + +
+
+
+ + + + {{form.get('party_id')?.errors?.message | + translate}} + + + + + {{'validator.number' | + translate}} + + + + + +
+
+
+ + + + + + + + {{form.get('endpoint')?.errors?.message | translate}} + +
+ + + + + {{'site.fmlManagerServerNameIPHelper' | translate}} + + {{form.get('fml_manager_server_name')?.errors?.message | translate}} + +
+ +
+
+ + {{'site.fateFlow' | translate}} + + + + + + {{form.get('fate_flow_host_ip')?.errors?.message | translate}} + + + + + + + {{'validator.number' | translate}} + + +
+ + + + + +
+
+ + {{'site.kubeflow' | translate}} + + +
+ + + + {{ + form.get('minio_endpoint')?.errors?.message | translate}} + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+
+
+ + + + {{'CommonlyUse.saving' | translate}} ... + +
+
\ No newline at end of file diff --git a/site-portal/frontend/src/app/view/site-config/site-config.component.spec.ts b/site-portal/frontend/src/app/view/site-config/site-config.component.spec.ts new file mode 100644 index 00000000..bc4e91ee --- /dev/null +++ b/site-portal/frontend/src/app/view/site-config/site-config.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SiteConfigComponent } from './site-config.component'; + +describe('SiteConfigComponent', () => { + let component: SiteConfigComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SiteConfigComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SiteConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/site-config/site-config.component.ts b/site-portal/frontend/src/app/view/site-config/site-config.component.ts new file mode 100644 index 00000000..2c841121 --- /dev/null +++ b/site-portal/frontend/src/app/view/site-config/site-config.component.ts @@ -0,0 +1,387 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, AfterViewChecked, ChangeDetectorRef, AfterContentChecked, OnDestroy } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { SiteConfigureService } from '../../service/site-configure.service'; +import { SiteService } from '../../service/site.service'; +import '@cds/core/icon/register.js'; +import { checkCircleIcon, checkIcon, ClarityIcons, errorStandardIcon, infoCircleIcon } from '@cds/core/icon'; +import { Router, ActivatedRoute } from '@angular/router'; +import { MessageService } from '../../components/message/message.service' +import { ValidatorGroup } from '../../../config/validators' + +ClarityIcons.addIcons(checkIcon, errorStandardIcon, checkCircleIcon, infoCircleIcon); + +@Component({ + selector: 'app-site-config', + templateUrl: './site-config.component.html', + styleUrls: ['./site-config.component.css'] +}) +export class SiteConfigComponent implements OnInit, AfterViewChecked, AfterContentChecked, OnDestroy { + siteInfo: any = ''; + siteUpdatedInfo: any = { + description: "", + external_host: "", + external_port: 0, + https: false, + fate_flow_grpc_port: 0, + fate_flow_host: "", + fate_flow_http_port: 0, + fml_manager_endpoint: "", + fml_manager_server_name: "", + id: 0, + kubeflow_config: { + kubeconfig: "", + minio_access_key: "", + minio_endpoint: "", + minio_region: "", + minio_secret_key: "", + minio_ssl_enabled: false, + }, + name: "", + party_id: 0, + uuid: "" + } + + // form for update site configuration + form = this.fb.group( + ValidatorGroup([ + { + name: 'name', + value: '', + type: ['word'], + max: 20, + min: 2 + }, + { + name: 'site_ip', + type: ['notRequired', 'endpointWithoutPort'], + value: '' + }, + { + name: 'site_https_string', + type: [''], + value: '' + }, + { + name: 'party_id', + type: ['notRequired','number'], + value: 0 + }, + { + name: 'site_port', + type: ['notRequired', 'number'], + value: '' + }, + { + name: 'desc', + type: [''] + }, + { + name: 'endpoint', + type: ['notRequired', 'endpoint'], + value: '' + }, + { + name: 'fml_https_string', + type: [''], + value: '' + }, + { + name: 'fml_manager_server_name', + type: [''], + value: '' + }, + { + name: 'fate_flow_host_ip', + type: ['notRequired'], + value: '' + }, + { + name: 'http_port', + type: ['notRequired','number'], + value: '' + }, + { + name: 'minio_endpoint', + type: ['notRequired', 'endpoint'], + value: '' + }, + { + name: 'access_key', + type: [''], + value: '' + }, + { + name: 'secret_key', + type: [''], + value: '' + }, + { + name: 'kubeconfig', + type: [''], + value: '' + }, + { + name: 'minio_ssl_enabled', + type: ['notRequired', ''], + value: false + }, + ]) + ); + + + name: string = ""; + party_id = 0; + site_ip: string = ""; + site_port = 0; + site_https_string = ""; + desc: string = ""; + endpoint: string = ""; + fate_flow_host_ip: string = ""; + http_port = 0; + minio_endpoint: string = ""; + access_key: string = ""; + secret_key: string = ""; + kubeconfig: string = ""; + minio_region: string = ""; + minio_ssl_enabled: boolean = false; + // fml_manager_connected is the current status of fml manager connection for site + fml_manager_connected: boolean = false; + fml_https_string = "http" + // fml_manager_endpoint_address is the full address (inclues http:// or https:// prefix) + fml_manager_endpoint_address = ""; + fml_manager_server_name = "" + fate_flow_connected: boolean = false; + kubeflow_connected: boolean = false; + isRegisterToFMLManagerSubmitted: boolean = false; + isConnectFailed: boolean = false; + isTestFATEFlowSubmit = false + isTestFATEFlowFailed = false + panelOpen1: boolean = true; + panelOpen2: boolean = true; + panelOpen3: boolean = true; + errorMessage: string = ""; + isUpdateFailed: boolean = false; + isUpdateSubmit: boolean = false; + constructor(private fb: FormBuilder, private siteService: SiteService, private siteConfigService: SiteConfigureService, private route: ActivatedRoute, + private router: Router, private msg: MessageService, private cdRef: ChangeDetectorRef) { + this.getSiteConfiguration(); + } + ngOnDestroy(): void { + this.msg.close() + } + ngAfterContentChecked(): void { + this.cdRef.detectChanges() + } + + ngAfterViewChecked() { + this.cdRef.detectChanges() + } + ngOnInit(): void { + } + + // getSiteConfiguration is to get the site configuration + getSiteConfiguration() { + this.siteService.getSiteInfo() + .subscribe((data: any) => { + this.siteInfo = data.data; + this.name = this.siteInfo.name; + this.party_id = this.siteInfo.party_id; + this.desc = this.siteInfo.description; + this.fml_manager_connected = this.siteInfo.fml_manager_connected; + this.fate_flow_connected = this.siteInfo.fate_flow_connected; + this.kubeflow_connected = this.siteInfo.kubeflow_connected; + this.site_ip = this.siteInfo.external_host; + this.site_port = this.siteInfo.external_port; + this.site_https_string = this.siteInfo.https ? "https" : "http"; + this.fate_flow_host_ip = this.siteInfo.fate_flow_host; + this.http_port = this.siteInfo.fate_flow_http_port; + this.kubeconfig = this.siteInfo.kubeflow_config.kubeconfig; + this.access_key = this.siteInfo.kubeflow_config.minio_access_key; + this.minio_endpoint = this.siteInfo.kubeflow_config.minio_endpoint; + this.minio_region = this.siteInfo.kubeflow_config.minio_region; + this.secret_key = this.siteInfo.kubeflow_config.minio_secret_key; + this.minio_ssl_enabled = this.siteInfo.kubeflow_config.minio_ssl_enabled; + this.fml_manager_server_name = this.siteInfo.fml_manager_server_name; + this.fml_manager_endpoint_address = this.siteInfo.fml_manager_endpoint; + this.parseFMLManagerEndpoint(this.fml_manager_endpoint_address); + if (this.fml_manager_connected) { + this.setDisbled(true) + } else { + this.setDisbled(false) + } + }); + } + + // parseFMLManagerEndpoint is to parse the endpoint tp get the ip without http or https header + parseFMLManagerEndpoint(fml_manager_endpoint_address: string) { + if (fml_manager_endpoint_address != "") { + var strList = fml_manager_endpoint_address.split("://"); + this.fml_https_string = strList[0]; + this.endpoint = strList[1]; + } + } + + // enableFMLManagerServerName is the flag to alert user to input the 'fml manager server name' when user choose to connect fml manager via https + get enableFMLManagerServerName() { + return this.fml_https_string === "https" + } + + // fmlManagerEndpointIsIP is to validate is fml manager endpoint address is IP address or FQDN (if is IP, alert user to input the 'fml manager server name') + get fmlManagerEndpointIsIP() { + const reg = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g + return this.fml_manager_server_name === "" && reg.test(this.endpoint?.trim()) + } + + // registerToFMLManager is to send request to register to fml manager + registerToFMLManager() { + this.isRegisterToFMLManagerSubmitted = true; + // check if the form is change, if yes, alert user to save change first + this.formOnChange(); + if (this.isFormChanged) { + this.openModal = true; + return; + } + this.fml_manager_endpoint_address = this.endpoint === "" ? "" : this.fml_https_string + "://" + this.endpoint + var connectInfo = { + endpoint: this.fml_manager_endpoint_address?.replace(/\s/g, ""), + server_name: this.fml_manager_server_name?.trim() + } + this.siteConfigService.connectFML(connectInfo) + .subscribe( + data => { + this.isConnectFailed = false; + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isConnectFailed = true; + }); + } + + testFATEFlowSuccess = false + // testFATEFlow is to test the FATEFlow connection + testFATEFlow() { + this.isTestFATEFlowSubmit = true + this.isTestFATEFlowFailed = false + this.testFATEFlowSuccess = false + var https = false; + this.siteConfigService.testFATEFlow(this.fate_flow_host_ip.replace(/\s/g, ""), https, Number(this.http_port)) + .subscribe( + data => { + this.testFATEFlowSuccess = true + this.isTestFATEFlowSubmit = false + this.msg.success('serverMessage.default200', 1000) + }, + err => { + this.errorMessage = err.error.message; + this.isTestFATEFlowFailed = true; + this.isTestFATEFlowSubmit = false + }); + } + + isTestKubeFlowSubmit = false + isTestKubeFlowFailed = false + isTestKubeFlowSuccess = false + // testKubeFlow is to test the connection with KubeFlow + testKubeFlow() { + this.isTestKubeFlowSubmit = true + this.isTestKubeFlowFailed = false + this.isTestKubeFlowSuccess = false + this.siteConfigService.testKubeFlow(this.kubeconfig, this.access_key.replace(/\s/g, ""), this.minio_endpoint.replace(/\s/g, ""), this.minio_region, this.secret_key.replace(/\s/g, ""), this.minio_ssl_enabled) + .subscribe( + data => { + this.msg.success('serverMessage.default200', 1000) + this.isTestKubeFlowSuccess = true + this.isTestKubeFlowSubmit = false + }, + err => { + this.errorMessage = err.error.message + this.isTestKubeFlowFailed = true + this.isTestKubeFlowSubmit = false + }); + } + + isFormChanged: boolean = false; + openModal: boolean = false; + // formOnChange is to detect if the form is changed + formOnChange() { + if (this.name != this.siteInfo.name || this.party_id != this.siteInfo.party_id || this.site_ip != this.siteInfo.external_host || this.site_port != this.siteInfo.external_port) { + this.isFormChanged = true; + } + if ((this.siteInfo.https && this.site_https_string === "http") || (!this.siteInfo.https && this.site_https_string === "https")) { + this.isFormChanged = true; + } + this.fml_manager_endpoint_address = this.endpoint === "" ? "http://" : this.fml_https_string + "://" + this.endpoint + } + + // saveSiteConfigUpdate is to save the updated site config + saveSiteConfigUpdate() { + this.isUpdateFailed = false; + this.isUpdateSubmit = true; + if (!this.form.valid) { + this.errorMessage = "Invalid input"; + this.isUpdateFailed = true; + return; + } + this.siteUpdatedInfo.name = this.name; + this.siteUpdatedInfo.party_id = Number(this.party_id); + this.siteUpdatedInfo.external_host = this.site_ip.replace(/\s/g, ""); + this.siteUpdatedInfo.external_port = Number(this.site_port); + this.siteUpdatedInfo.https = this.site_https_string === "https" ? true : false; + this.siteUpdatedInfo.description = this.desc; + this.fml_manager_endpoint_address = this.endpoint === "" ? "" : this.fml_https_string + "://" + this.endpoint + this.siteUpdatedInfo.fml_manager_endpoint = this.fml_manager_endpoint_address.replace(/\s/g, ""); + this.siteUpdatedInfo.fml_manager_server_name = this.fml_manager_server_name.trim(); + this.siteUpdatedInfo.fate_flow_host = this.fate_flow_host_ip.replace(/\s/g, ""); + this.siteUpdatedInfo.fate_flow_http_port = Number(this.http_port); + this.siteUpdatedInfo.kubeflow_config.minio_endpoint = this.minio_endpoint.replace(/\s/g, ""); + this.siteUpdatedInfo.kubeflow_config.minio_access_key = this.access_key.replace(/\s/g, ""); + this.siteUpdatedInfo.kubeflow_config.minio_secret_key = this.secret_key.replace(/\s/g, ""); + this.siteUpdatedInfo.kubeflow_config.kubeconfig = this.kubeconfig; + this.siteUpdatedInfo.kubeflow_config.minio_ssl_enabled = this.minio_ssl_enabled; + this.siteConfigService.putConfigUpdate(this.siteUpdatedInfo) + .subscribe(data => { + this.msg.success('serverMessage.modify200', 1000) + this.reloadCurrentRoute(); + }, + err => { + this.errorMessage = err.error.message; + this.isUpdateFailed = true; + }); + } + + // setDisbled is to disable the selected input container (when site is successfully connected to fml manager, disabled the field below) + setDisbled(opt: boolean) { + var keys = ["site_https_string", "site_ip", "site_port"] + for (const key of keys) { + const val = this.form.get(key) + if (val) { + if (opt) { + val.disable() + } else { + val.enable() + } + } + } + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + +} diff --git a/site-portal/frontend/src/app/view/user-mg/user-mg.component.css b/site-portal/frontend/src/app/view/user-mg/user-mg.component.css new file mode 100644 index 00000000..51cae387 --- /dev/null +++ b/site-portal/frontend/src/app/view/user-mg/user-mg.component.css @@ -0,0 +1,24 @@ +.content-area{ + width: 100%; + min-width: 1080px; +} +.tabletext{ + text-align: left; +} +.checkbox1{ + position: relative; + border-radius: 5px; +} +clr-checkbox-container { + text-align: left; + display:block; + margin:0px auto; +} +.pageLoading { + margin-top: 10%; + margin-left: 45%; +} +.refreshbtn { + float: right; + margin-right: 12px; +} \ No newline at end of file diff --git a/site-portal/frontend/src/app/view/user-mg/user-mg.component.html b/site-portal/frontend/src/app/view/user-mg/user-mg.component.html new file mode 100644 index 00000000..2a654f38 --- /dev/null +++ b/site-portal/frontend/src/app/view/user-mg/user-mg.component.html @@ -0,0 +1,65 @@ +
+
+

{{'nav.userMg'|translate}}

+
+
+
+ + + +
+ + {{'userMg.userId'|translate}} + {{'userMg.userName'|translate}} + {{'userMg.sitePortal'|translate}} + {{'userMg.FATEboard'|translate}} + {{'userMg.jupyterNotebook'|translate}} + + {{user.id}} + {{user.name}} + + + + + + + + + + + + + + + + + + + + + + + {{userList ? userList.length : 0}} {{'CommonlyUse.item' | translate}} + +
+
+ + + + {{'userMg.saving' | translate}}... + +
diff --git a/site-portal/frontend/src/app/view/user-mg/user-mg.component.spec.ts b/site-portal/frontend/src/app/view/user-mg/user-mg.component.spec.ts new file mode 100644 index 00000000..cc1bde29 --- /dev/null +++ b/site-portal/frontend/src/app/view/user-mg/user-mg.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserMgComponent } from './user-mg.component'; + +describe('UserMgComponent', () => { + let component: UserMgComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserMgComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserMgComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/site-portal/frontend/src/app/view/user-mg/user-mg.component.ts b/site-portal/frontend/src/app/view/user-mg/user-mg.component.ts new file mode 100644 index 00000000..4e67cb06 --- /dev/null +++ b/site-portal/frontend/src/app/view/user-mg/user-mg.component.ts @@ -0,0 +1,135 @@ +// Copyright 2022 VMware, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; +import { UserMgService } from '../../service/user-mg.service'; +import { MessageService } from '../../components/message/message.service' + +export interface UserResponse { + Code: number, + Message: string, + Data: PublicUser[], +} +export interface PermisionResponse { + Code: number, + Message: string, + Data: {}, +} +export interface PublicUser { + fateboard_access: boolean, + id: number, + name: string, + notebook_access: boolean, + site_portal_access: boolean, + uuid: string, +} +export interface Permission { + fateboard_access: boolean, + notebook_access: boolean, + site_portal_access: boolean, +} + +@Component({ + selector: 'app-user-mg', + templateUrl: './user-mg.component.html', + styleUrls: ['./user-mg.component.css'] +}) + +export class UserMgComponent implements OnInit, OnDestroy { + userResponse: any; + userList: any; + // preUserList is Deep Copy of user list for later compare + preUserList: any; + + constructor(private userService: UserMgService, private router: Router, private msg: MessageService) { + this.getUserList(); + } + + isPageLoading: boolean = true; + isGetUserListFailed: boolean = false; + // getUserList is to get current user list + getUserList() { + this.isPageLoading = true; + this.isGetUserListFailed = false; + this.userService.getUser() + .subscribe((data: UserResponse) => { + this.userResponse = data; + this.userList = this.userResponse.data; + //Deep Copy + this.preUserList = JSON.parse(JSON.stringify(this.userList)); + this.isPageLoading = false; + }, + err => { + this.errorMessage = err.error.message; + this.isGetUserListFailed = true; + this.isPageLoading = false; + + } + ); + } + + ngOnInit(): void { + } + ngOnDestroy(): void { + this.msg.close() + } + + isUpdateSubmit: boolean = false; + isUpdateFailed: boolean = false; + errorMessage: string = ''; + // updateUserPermission is to update user permission + updateUserPermission() { + for (let i = 0; i < this.userList.length; i++) { + // validate if the user permission options is changed + var user = this.userList[i]; + var preUser = this.preUserList[i]; + var prePermission = { + fateboard_access: preUser.fateboard_access, + notebook_access: preUser.notebook_access, + site_portal_access: preUser.site_portal_access + } + var curPermission = { + fateboard_access: user.fateboard_access, + notebook_access: user.notebook_access, + site_portal_access: user.site_portal_access + } + //if is not changed, skip + if (prePermission.fateboard_access === curPermission.fateboard_access && prePermission.site_portal_access === curPermission.site_portal_access + && prePermission.notebook_access === curPermission.notebook_access) continue; + this.isUpdateSubmit = true; + this.isUpdateFailed = false; + this.userService.updatePermision(curPermission, user.id) + .subscribe(data => { + this.msg.success('serverMessage.default200', 1000) + this.reloadCurrentRoute(); + }, + err => { + this.isUpdateFailed = true; + this.errorMessage = err.error.message; + } + ); + } + } + + //reloadCurrentRoute is to reload current page + reloadCurrentRoute() { + let currentUrl = this.router.url; + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate([currentUrl]); + }); + } + + // refresh button + refresh() { + this.getUserList(); + } +} diff --git a/site-portal/frontend/src/assets/.gitkeep b/site-portal/frontend/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/site-portal/frontend/src/assets/close.jpeg b/site-portal/frontend/src/assets/close.jpeg new file mode 100644 index 00000000..04bb0f62 Binary files /dev/null and b/site-portal/frontend/src/assets/close.jpeg differ diff --git a/site-portal/frontend/src/assets/close.jpg b/site-portal/frontend/src/assets/close.jpg new file mode 100644 index 00000000..02027cd2 Binary files /dev/null and b/site-portal/frontend/src/assets/close.jpg differ diff --git a/site-portal/frontend/src/assets/i18n/en.json b/site-portal/frontend/src/assets/i18n/en.json new file mode 100644 index 00000000..3ea22815 --- /dev/null +++ b/site-portal/frontend/src/assets/i18n/en.json @@ -0,0 +1,341 @@ +{ + "validator": { + "empty": "This field is required.", + "email": "Please enter the correct email address", + "number": "Please enter numbers", + "word": "Can not contain special symbols", + "engu": "Please enter numbers and letters", + "internet": "Invalid endpoint: http:// or https:// schema is scheme", + "ip": "Please enter the correct IP", + "zeroToHundred": "Please enter a number between 0 and 100 (including 0 and 100)", + "noSpace":"Input can not contain space", + "json":"Invaild JSON format", + "match":"Passwords do not match.", + "password":"Password should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.", + "fileSize": "The maximum upload file size is d%", + "fileFormat": "The upload file format can only be: d%", + "endpoint":"Invaild endpoint address.", + "endpintWithoutPort": "Don't enter port number here" + }, + "serverMessage": { + "default200": "Successful operation", + "default401": "Please log in.", + "default404": "Error 404: request failed.", + "login200": "Delete successfully", + "delete200": "Delete successfully", + "create200": "Create successfully", + "upload200": "Upload successfully", + "download200": "Download successfully", + "modify200": "Modify successfully", + "update_password_success": "Password is successfully updated!", + "login_again_alert":"The page will redirect to login page in 3s." + }, + "home": { + "sitePortal": "FML Site Portal", + "logOut":"Log Out", + "changePassword": "Change Password", + "curPassword": "Current Password", + "newPassword": "New Password", + "confirmPassword": "Confirm Password" + }, + "nav": { + "siteInfo": "Site Information", + "dataMg": "Data Management", + "modelMg": "Model Management", + "projectMg": "Project Management", + "userMg": "User Management", + "siteConfiguration": "Site Configuration" + }, + "CommonlyUse": { + "name": "Name", + "ip": "FATE-FLow Address", + "port": "HTTP Port", + "unregister": "Unregister", + "register": "Register", + "test": "test", + "cancel" : "Cancel", + "Cancel" : "CANCEL", + "add": "ADD", + "modify": "Modify", + "Create": "CREATE", + "save": "Save", + "few": "At least ", + "character": " characters", + "many": "Up to ", + "description": "Description", + "all": "All", + "detail": "Detail", + "createTime": "Create Time", + "updateTime": "Update Time", + "action": "Action", + "actions": "Actions", + "closed": "Closed", + "siteName": "Site Name", + "back": "Back", + "refresh": "Refresh", + "id": "ID", + "status": "Status", + "select": "Select", + "version": "Version", + "ok": "Ok", + "joined": "Joined", + "pending": "Pending", + "delete": "Delete", + "yes": "YES", + "uploading": "Uploading", + "upload": "Upload", + "reset": "Reset", + "submit": "Submit", + "creating": "Creating", + "download":"Download", + "success":"Success", + "file":"File", + "project":"project", + "accept": "Accept", + "decline": "Decline", + "saving": "Saving", + "error":"Error", + "user":"user", + "deleting":"Deleting", + "model":"model", + "item":"item(s)", + "null": "N/A", + "true" : "True", + "false" : "False", + "leaving":"Leaving", + "pleasewait": "Please wait", + "drop": "Interactive", + "json": "JSON" + }, + "placeholder": { + "name": "Enter your Site name", + "ip": "Enter your Site IP", + "id": "Enter your Party Id", + "port": "Enter your Site Port" + }, + "projectMg": { + "newProject": "New Project", + "createNewProject": "Create New Project", + "autoApprovalOfJobs": "Auto-approval of jobs", + "projectList": "Project List", + "youHaveAJob": "You have a job invitation.", + "projectName": "Project Name", + "projectManager": "Project Manager", + "managingSiteName": "Managing Site Name", + "managingSitePartyID": "Managing Site Party ID", + "paticipant": "Paticipant", + "job": "Job", + "owned": "Owned", + "projectInvitation": "Project Invitation", + "close":"Close", + "leave":"Leave", + "invitationMessage":"You have a job invitation.", + "allProject": "All projects", + "ownedProject":"Owned projects", + "joinedProject":"Joined projects", + "closedProject":"Closed projects", + "leaveProjectMessage":"Do you want to leave this project?", + "rejectProjectMessage":"Do you want to decline to join this project?", + "leaveProjectDataError":"Please click here to cancel all associated data before leave the project.", + "closeProjectMessage":"Do you want to close this project?" + }, + "projectDetail": { + "projectInformation": "Project Information", + "jobManagement": "Job Management", + "participantManagement": "Participant Management", + "role": "Role", + "creator": "Creator", + "participant": "Participant", + "closeProject": "Close Project", + "leaveProject": "Leave Project", + "newJob": "New Job", + "finishTime": "Finish Time", + "jobType": "Job Type", + "initiator": "Initiator", + "associateData": "Associate Data", + "associateLocalData": "Associate Local Data", + "dataProvider": "Data Provider", + "providerPartyID": "Provider Party ID", + "cancelAssociation": "Cancel association", + "inviteNewParticipant": "Invite new participant", + "invite": "Invite", + "isRemoveParty": "Do you want to remove this party?", + "projectDetail":"Project Detail", + "invited":"Invited", + "revokeInvitation" : "Revoke Invitation", + "rejectMessage":"Do you want to decline to join this job?" + }, + "dataMg": { + "uploadLocalData": "Upload local data", + "dataID": "Data ID", + "profile": "Profile", + "uploadJobStatus": "Upload Job Status", + "isDeleteData": "Do you want to delete the data?", + "DataCanNotBeRecoveredAfterDeletion": "Data can not be recovered after deletion.", + "Featuresize":"Feature size", + "Samplesize": "Sample size" + }, + "dataDetail": { + "dataDetail" : "Data Detail", + "dataFile": "Data File", + "tableName": "Table Name", + "dataOverview": "Data Overview", + "sampleSize": "Sample Size", + "idMetadata": "ID Metadata", + "idType": "ID Type", + "cellphone": "Cellphone", + "device": "Device", + "other": "Other", + "deviceType": "Device type", + "encryption": "Encryption", + "plaintext": "Plaintext", + "ciphertext": "Ciphertext", + "ciphertextType": "Ciphertext type", + "featureDimensions": "Feature Dimensions", + "dataPreview": "Data Preview (only show the first ten rows)", + "deleteData": "Delete data", + "IMEI":"IMEI", + "IDFA":"IDFA", + "IDFV":"IDFV" + }, + "modelMg": { + "modelID": "Model ID", + "publish": "Publish", + "parameters_json":"Parameters JSON", + "jsonFormatMessage":"Please enter with JSON format" + }, + "modelDetail": { + "modelEvaluation": "Model Evaluation", + "modelVersion": "Model Version", + "metrics": "Metrics", + "relatedProjectJobs": "Related Project and Job", + "publishModel": "Publish the model", + "serviceName": "Service Name", + "componentName": "Component Name", + "deploymentType": "Deployment Type", + "KFServing": "KFServing", + "FATEServing": "FATE-Serving", + "DeleteMessage":"Do you want to delete this model?", + "modelDetail":"Model Detail", + "metric": "Metric", + "Projectname":"Project Name", + "Jobname":"Job Name" + }, + "newJob": { + "createNewJob": "Create a New Job", + "jobInformation": "Job Information", + "type": "Type", + "Modeling": "Modeling", + "Prediction": "Prediction", + "modelConfiguration": "Model Configuration", + "selectModel": "Select a model", + "dataConfiguration": "Data Configuration", + "editParticipantData": "Edit Participant and Data", + "associatedData": "Associated Data", + "labelColumn": "Label Column", + "ValidationData": "Validation Data (optional)", + "self": "Self", + "pleaseSelect": "Please select", + "modelName": "Model name", + "algorithm": "Algorithm", + "homoLogisticRegression": "Homo Logistic Regression", + "homoSecureBoost": "Homo SecureBoost", + "generateConfiguration": "Generate Configuration", + "algorithmConfiguration": "Algorithm Configuration", + "workflowDSL": "Workflow DSL", + "selectParticipantAndData": "Select participant and data", + "toAssociatedData": "Click here to associated data.", + "resetSelection": "Reset selection", + "componentsDeploy": "Components to deploy", + "valsetMessage":"Enter the percent of training set (0~100)", + "noSelection" : "No selection", + "noModel":"No available model.", + "noData":"No associated data available.", + "inputSource": "Please select input source", + "outputType": "output for d%:", + "construct": "Construct", + "currentModule": "Current Module", + "source": "Source", + "target": "Target" + }, + "jobDetail": { + "jobDetail": "Job Detail", + "accept": "Accept", + "decline": "Decline", + "jobName": "Job Name", + "jobDescription": "Job Description", + "jobID": "Job ID", + "FATEJobID": "FATE Job ID", + "FATEModelName": "FATE Model Name", + "statusMessage": "Status Message", + "initiatorData": "Initiator Data", + "initiatorSite": "Initiator Site", + "dataName": "Data Name", + "dataDescription": "Data Description", + "dataLabel": "Data Label", + "collaborativeDataFromOtherParticipant": "Collaborative data from other participant(s)", + "algorithmModuleName": "Algorithm module name", + "enableValidation": "Enable validation", + "validationDataSize": "Validation Data Size", + "algorithmType": "Algorithm Type", + "model": "Model", + "jobResult": "Job Result", + "modelEvaluation": "Model Evaluation", + "index": "Index", + "metric": "Metric", + "predictResult": "Predict Result", + "jobIs": "Job is ", + "deleteJob": "Do you want to delete the Job?", + "noData":"There is no data from the other site.", + "output":"Outputting d% instances (Only 100 instances are shown in the table)", + "validationSize" : "d%% of training dataset", + "intersectNumber": "Intersect Number", + "intersectRate": "Intersect Rate", + "only100": "(Only 100 instances are shown in the table)" + }, + "userMg": { + "userId": "User ID", + "userName": "User Name", + "sitePortal": "Site Portal", + "FATEboard": "FATE board", + "jupyterNotebook": "Jupyter Notebook", + "saving": "Saving" + }, + "site" : { + "siteIP": "Site Address", + "partyId": "Party ID", + "sitePort": "Site Port", + "fmlEndPoint": "FML Manager Endpoint", + "fateFlow": "FATE-Flow Configuration", + "kubeflow": "Kubeflow Configuration (Optional)", + "endpoint": "MinIO Endpoint", + "access": "MinIO Access Key", + "secret": "MinIO Secret Key", + "enableSSL": "Enable SSL", + "kubeConfig": "Kubeconfig", + "saveAlertMessage": "Please save all changes before registering for FML Manager.", + "fmlManagerServerName":"FML Manager Server Name", + "fmlManagerServerNameHelper1":"Site Portal uses 'FML Manager Server Name' to verify FML Manager's server certificate.", + "fmlManagerServerNameHelper2":"If you leave it empty, server name will be 'FML Manager Endpoint' address.", + "fmlManagerServerNameIPHelper":"You may need to provide 'Server Name' when 'FML Manager Endpoint' is an IP address." + }, + "login": { + "loginOut": "Log Out Success", + "welcome": "Welcome to", + "username": "Username", + "password": "Password", + "logIn": "Log In" + }, + "Modeling": "Modeling", + "Predict": "Predict", + "HomoLogisticRegression": "HomoLogisticRegression", + "HomoSecureBoost": "HomoSecureBoost", + "Pending": "Pending", + "Rejected": "Rejected", + "Running": "Running", + "Failed": "Failed", + "Succeeded": "Succeeded", + "en": "English", + "zh_CN": "中文 (简体)", + "Owner": "Owner" +} diff --git a/site-portal/frontend/src/assets/i18n/zh_CN.json b/site-portal/frontend/src/assets/i18n/zh_CN.json new file mode 100644 index 00000000..21759686 --- /dev/null +++ b/site-portal/frontend/src/assets/i18n/zh_CN.json @@ -0,0 +1,341 @@ +{ + "validator": { + "empty": "当前内容是必填项", + "email": "请输入正确的邮箱地址", + "number": "请输入数字", + "word": "不允许包含特殊符号", + "engu": "只能输入数字与字母", + "internet": "endpoint格式不合理: 请用http:// 或者 https:// 的格式", + "ip": "请输入正确的IP地址", + "zeroToHundred": "请输入0至100之间的数(包含0和100)", + "noSpace":"输入不能有空格", + "json":"不规范的JSON格式", + "match":"两次密码不相同", + "password":"新密码长度必须在8-20之间,至少有一个大写字母,一个小写字母,和一个数字", + "fileSize": "文件最大容量是d%", + "fileFormat": "文件格式只能为:d%", + "endpoint":"终端地址不合法", + "endpintWithoutPort": "不要在这里输入端口号" + }, + "serverMessage": { + "default200": "操作成功", + "default401": "请登录!", + "default404": "错误404: 请求失败", + "login200": "登录成功", + "delete200": "删除成功", + "create200": "创建成功", + "upload200": "上传成功", + "download200": "下载成功", + "modify200": "修改成功", + "update_password_success": "密码修改成功!", + "login_again_alert":"页面将于3秒后跳转到登录界面。" + }, + "home": { + "sitePortal": "FML Site Portal", + "logOut":"退出", + "changePassword": "修改密码", + "curPassword": "当前密码", + "newPassword": "新密码", + "confirmPassword": "确认密码" + }, + "nav": { + "siteInfo": "站点信息", + "dataMg": "数据管理", + "modelMg": "模型管理", + "projectMg": "项目管理", + "userMg": "用户管理", + "siteConfiguration": "站点配置" + }, + "CommonlyUse": { + "name": "名称", + "ip": "FATE-Flow 地址", + "port": "端口", + "unregister": "注销", + "register": "登记", + "test": "测试", + "cancel" : "取消", + "Cancel" : "取消", + "add": "添加", + "modify": "修改", + "Create": "创建", + "save": "保存", + "few": "至少 ", + "character": " 字符", + "many": "最多 ", + "description": "描述", + "all": "全部", + "detail": "详情", + "createTime": "创建时间", + "updateTime": "更新时间", + "action": "操作", + "actions": "操作", + "closed": "关闭", + "siteName": "站点名称", + "back": "返回", + "refresh": "刷新", + "id": "ID", + "status": "状态", + "select": "选择", + "version": "版本", + "ok": "确定", + "joined": "已加入", + "pending": "等待中", + "delete": "删除", + "yes": "确定", + "uploading": "上传中", + "upload": "上传", + "reset": "重置", + "submit": "提交", + "creating": "创建中", + "download":"下载", + "success":"成功", + "file":"文件", + "project":"项目", + "accept": "接受", + "decline": "拒绝", + "saving": "保存", + "error":"错误", + "user":"用户", + "deleting":"删除中", + "model":"模型", + "item":"项", + "null": "未知", + "true" : "是", + "false" : "否", + "leaving":"离开中", + "pleasewait": "请等待", + "drop": "交互式", + "json": "JSON" + }, + "placeholder": { + "name": "请输入您的站点名称", + "ip": "请输入您的站点IP", + "id": "请输入您的组织 Id", + "port": "请输入您的站点端口号" + }, + "projectMg": { + "newProject": "新项目", + "createNewProject": "创建新项目", + "autoApprovalOfJobs": "自动批准所有任务邀请", + "projectList": "项目列表", + "youHaveAJob": "你有一个任务邀请", + "projectName": "项目名称", + "projectManager": "项目管理者", + "managingSiteName": "项目管理者站点名称", + "managingSitePartyID": "项目管理者站点ID", + "paticipant": "成员", + "job": "任务", + "owned": "拥有的", + "projectInvitation": "项目邀请", + "close":"关闭", + "leave":"离开", + "invitationMessage":"你有一个任务邀请", + "allProject": "所有项目", + "ownedProject":"我的项目", + "joinedProject":"加入的项目", + "closedProject":"关闭的项目", + "leaveProjectMessage":"你想要离开这个项目吗?", + "rejectProjectMessage":"你想要拒绝加入这个项目吗?", + "leaveProjectDataError":"离开此项目前请取消关联所有的数据", + "closeProjectMessage":"你想要关闭这个项目吗?" + }, + "projectDetail": { + "projectInformation": "项目信息", + "jobManagement": "任务管理", + "participantManagement": "成员管理", + "role": "角色", + "creator": "发起者", + "participant": "成员", + "closeProject": "关闭项目", + "leaveProject": "离开项目", + "newJob": "新任务", + "finishTime": "完成时间", + "jobType": "任务类型", + "initiator": "发起者", + "associateData": "关联数据", + "associateLocalData": "本地关联数据", + "dataProvider": "数据提供者", + "providerPartyID": "数据提供者ID", + "cancelAssociation": "取消关联", + "inviteNewParticipant": "邀请新成员", + "invite": "邀请", + "isRemoveParty": "你想要移出这个成员吗?", + "projectDetail":"项目详情", + "invited":"已邀请", + "revokeInvitation" : "撤销邀请", + "rejectMessage":"你想要拒绝加入这个任务吗?" + }, + "dataMg": { + "uploadLocalData": "上传本地数据", + "dataID": "数据 ID", + "profile": "简介", + "uploadJobStatus": "上传任务状态", + "isDeleteData": "您想要删除这条数据吗?", + "DataCanNotBeRecoveredAfterDeletion": "数据删除后无法被恢复", + "Featuresize":"特征量", + "Samplesize": "样本量" + }, + "dataDetail": { + "dataDetail" : "数据详情", + "dataFile": "数据文件", + "tableName": "表格名称", + "dataOverview": "数据概览", + "sampleSize": "样本数量", + "idMetadata": "ID Metadata", + "idType": "ID 类型", + "cellphone": "手机", + "device": "设备", + "other": "其他", + "deviceType": "设备类型", + "encryption": "加密", + "plaintext": "纯文本", + "ciphertext": "密码", + "ciphertextType": "密码类型", + "featureDimensions": "特征大小", + "dataPreview": "数据预览 (仅显示前十行)", + "deleteData": "删除数据", + "IMEI":"IMEI", + "IDFA":"IDFA", + "IDFV":"IDFV" + }, + "modelMg": { + "modelID": "模型 ID", + "publish": "发布", + "parameters_json":"参数JSON", + "jsonFormatMessage":"请输入JSON格式" + }, + "modelDetail": { + "modelEvaluation": "模型评价", + "modelVersion": "模型版本", + "metrics": "指标", + "relatedProjectJobs": "相关项目和任务", + "publishModel": "发布模型", + "serviceName": "Service名称", + "componentName": "Component 名称", + "deploymentType": "Deployment类型", + "KFServing": "KFServing", + "FATEServing": "FATE-Serving", + "DeleteMessage":"你想要删除这个模型吗?", + "modelDetail":"模型详情", + "metric": "指标", + "Projectname":"项目名称", + "Jobname":"任务名称" + }, + "newJob": { + "createNewJob": "创建一个新任务", + "jobInformation": "任务信息", + "type": "类型", + "Modeling": "模型训练", + "Prediction": "预测", + "modelConfiguration": "模型配置", + "selectModel": "选择一个模型", + "dataConfiguration": "数据配置", + "editParticipantData": "编辑成员和数据", + "associatedData": "关联数据", + "labelColumn": "标签列", + "ValidationData": "验证集 (可选填)", + "self": "自己", + "pleaseSelect": "请选择", + "modelName": "模型名称", + "algorithm": "算法", + "homoLogisticRegression": "Homo Logistic Regression", + "homoSecureBoost": "Homo SecureBoost", + "generateConfiguration": "生成配置文件", + "algorithmConfiguration": "算法配置", + "workflowDSL": "工作流DSL", + "selectParticipantAndData": "选择成员和数据", + "toAssociatedData": "点击这里去关联数据", + "resetSelection": "重置选择", + "componentsDeploy": "需要部署的组件", + "valsetMessage":"请输入训练集的比例(0~100)", + "noSelection" : "未选", + "noModel":"没有可用的模型", + "noData":"没有可用的数据", + "inputSource": "请选择输入数据模块", + "outputType": "d%模块输出类型", + "construct": "构建", + "currentModule": "当前模块", + "source": "来源", + "target": "目标" + }, + "jobDetail": { + "jobDetail": "任务详情", + "accept": "接受", + "decline": "拒绝", + "jobName": "任务名称", + "jobDescription": "任务描述", + "jobID": "任务 ID", + "FATEJobID": "FATE 任务ID", + "FATEModelName": "FATE 模型名称", + "statusMessage": "状态信息", + "initiatorData": "发起者数据", + "initiatorSite": "发起者站点", + "dataName": "数据名称", + "dataDescription": "数据描述", + "dataLabel": "数据标签", + "collaborativeDataFromOtherParticipant": "来自于其他成员的数据", + "algorithmModuleName": "算法名称", + "enableValidation": "开启验证", + "validationDataSize": "验证集大小", + "algorithmType": "算法类型", + "model": "模型", + "jobResult": "任务结果", + "modelEvaluation": "模型评价", + "index": "", + "metric": "指标", + "predictResult": "预测结果", + "jobIs": "任务", + "deleteJob": "你想要删除这个任务吗", + "noData": "没有来自于其他站点的数据", + "output":"共输出 d% 个实例 (表格中仅显示100个实例)", + "validationSize" : "数据集的d%%", + "intersectNumber": "Intersect Number", + "intersectRate": "Intersect Rate", + "only100": "(表格中仅显示100个实例)" + }, + "userMg": { + "userId": "用户ID", + "userName": "用户名", + "sitePortal": "Site Portal", + "FATEboard": "FATE board", + "jupyterNotebook": "Jupyter Notebook", + "saving": "保存中" + }, + "site" : { + "siteIP": "站点地址", + "partyId": "组织ID", + "sitePort": "站点端口号", + "fmlEndPoint": "FML Manager 终端", + "fateFlow": "FATE-Flow 配置", + "kubeflow": "Kubeflow 配置 (可选)", + "endpoint": "MinIO Endpoint", + "access": "MinIO 访问密钥", + "secret": "MinIO 私钥", + "enableSSL": "开启 SSL", + "kubeConfig": "配置文件", + "saveAlertMessage": "在注册FML Manager前, 请先保存所有更改", + "fmlManagerServerName":"FML Manager服务器名称", + "fmlManagerServerNameHelper1":"Site Portal 使用 'FML Manager服务器名称' 来验证 FML Manager的服务器证书.", + "fmlManagerServerNameHelper2":"如果你把它留空, 服务器名称将会是'FML Manager终端'地址.", + "fmlManagerServerNameIPHelper":"你或许需要提供'服务器名称' 当'FML Manager终端' 是一个IP地址的时候。" + }, + "login": { + "loginOut": "登录成功", + "welcome": "欢迎使用", + "username": "用户名", + "password": "密码", + "logIn": "登录" + }, + "Modeling": "模型训练", + "Predict": "预测", + "HomoLogisticRegression": "HomoLogisticRegression", + "HomoSecureBoost": "HomoSecureBoost", + "Pending": "等待中", + "Rejected": "已拒绝", + "Running": "运行中", + "Failed": "已失败", + "Succeeded": "已成功", + "en": "English", + "zh_CN": "中文(简体)", + "Owner": "所有者" +} diff --git a/site-portal/frontend/src/assets/search-line.svg b/site-portal/frontend/src/assets/search-line.svg new file mode 100644 index 00000000..2a7a92ce --- /dev/null +++ b/site-portal/frontend/src/assets/search-line.svg @@ -0,0 +1,5 @@ + + search-line + + + \ No newline at end of file diff --git a/site-portal/frontend/src/assets/siteportal-icon.png b/site-portal/frontend/src/assets/siteportal-icon.png new file mode 100644 index 00000000..d1a77cf3 Binary files /dev/null and b/site-portal/frontend/src/assets/siteportal-icon.png differ diff --git a/site-portal/frontend/src/assets/zoom.png b/site-portal/frontend/src/assets/zoom.png new file mode 100644 index 00000000..b53a5b49 Binary files /dev/null and b/site-portal/frontend/src/assets/zoom.png differ diff --git a/site-portal/frontend/src/config/constant.ts b/site-portal/frontend/src/config/constant.ts new file mode 100644 index 00000000..46fe8f2f --- /dev/null +++ b/site-portal/frontend/src/config/constant.ts @@ -0,0 +1,84 @@ +interface ConstantModel { + [key: string]: number +} +interface ResModel { + name: string + value: number +} +export const JOBTYPE: ConstantModel = { + Modeling: 1, + Predict: 2, + PSI: 3, + HomoLogisticRegression: 1, + HomoSecureBoost: 2 +} +export const JOBTRAININGTYPE: ConstantModel = { + HomoLogisticRegression: 1, + HomoSecureBoost: 2 +} +export const JOBSTATUS: ConstantModel = { + Pending: 1, + Rejected: 2, + Running: 3, + Failed: 4, + Succeeded: 5, + Deploying: 6 +} +export const PARTYSTATUS: ConstantModel = { + Unknown: 0, + Owner: 1, + Pending: 2, + Joined: 3, + Rejected: 4, + Left: 5, + Dismissed: 6, + Revoked: 7 +} +export const MODELDEPLOYTYPE: ConstantModel = { + Unknown: 0, + KFserving: 1 +} +export function constantGather(inter: string, state: number) { + let res: ResModel + switch (inter) { + case 'jobtype': + res = forObj(JOBTYPE, state) + break; + case 'jobstatus': + res = forObj(JOBSTATUS, state) + break; + case 'jobtrainingtype': + res = forObj(JOBTRAININGTYPE, state) + break; + case 'partyStatus': + res = forObj(PARTYSTATUS, state) + break; + default: + res = { + name: '', + value: 0 + } + break; + case 'modeldeploytype': + res = forObj(MODELDEPLOYTYPE, state) + break; + } + return res +} + +function forObj(obj: any, state: number): ResModel { + let res: ResModel = { + name: '', + value: 0 + } + for (const key in obj) { + if (obj[key] === state) { + res = { + name: key, + value: state + } + break + } + } + return res +} \ No newline at end of file diff --git a/site-portal/frontend/src/config/dag-drag.ts b/site-portal/frontend/src/config/dag-drag.ts new file mode 100644 index 00000000..82bd0ae9 --- /dev/null +++ b/site-portal/frontend/src/config/dag-drag.ts @@ -0,0 +1,355 @@ +import * as dagreD3 from 'dagre-d3' +import * as d3 from 'd3' +export default class Dag { + g: any; + dagModify: any + that: any + node_shape = "rect"; + node_stype = "fill:#fff;stroke:#000;"; + edge_stype = "fill:#fff;stroke:#333;stroke-width:1.5px"; + tooltip_css = "tooltip"; + rankdir = "TB"; + attrForm: { [key: string]: any } = { + moduleName: '', + options: 'common', + attrList: [], + diffList: [], + flag: true + } + startStr = 'reader_0' + dsl: any; + canva_selector: any; + conf: any; + d3: any + zoomMultiples = 1 + constructor(dsl: any, canva_selector: any, callback: Function, that: any) { + // data source + this.that = that + this.dsl = dsl; + this.dagModify = callback + for (const key in dsl) { + if (key === this.startStr) { + this.attrForm.moduleName = this.startStr + for (const attr in dsl[key].attributes) { + if (this.dsl[key].attributes[attr].hasOwnProperty('drop_down_box')) { + + this.attrForm.attrList.push({ + name: attr, + value: this.dsl[key].attributes[attr] + }) + } else { + this.attrForm.attrList.push({ + name: attr, + value: JSON.stringify(this.dsl[key].attributes[attr]) + }) + } + // this.attrForm.attrList.push({ + // name: attr, + // value: dsl[key][attr], + // }) + } + } + } + this.canva_selector = canva_selector + this.d3 = d3 + } + + get_source_data(input_filed: any): any { + var source_inputs = new Array(); + for (var key in input_filed) { + if (!Array.isArray(input_filed[key])) { + return this.get_source_data(input_filed[key]); + } else { + input_filed[key].forEach((val: any) => { + source_inputs.push(val); + }); + } + } + return source_inputs + } + + handle_common_components(component_name: any, common_components: any, results: any) { + var iter = function (obj: any, results: any) { + for (var key in obj) { + if (key == component_name) { + results.push(obj[key]) + return results; + } else { + if (typeof obj[key] == "object") { + iter(obj[key], results); + } else { + return results; + } + } + } + + return results + } + + return iter(common_components, results) + } + + handle_role_components(component_name: any, role_components: any, results: any) { + var found = false + var iter = function (obj: any, results: any) { + for (var key in obj) { + if (key == component_name) { + found = true + results.push(obj[key]) + return results; + } else { + if (key == "guest" || key == "host") { + results.push({ + "role": key + }) + } + + if (typeof obj[key] == "object") { + iter(obj[key], results); + } else { + return results; + } + } + } + if (!found) { + results.pop("role") + } + + return results + } + + return iter(role_components, results) + } + + get_component_para(component_name: any) { + var components_paras = this.conf["component_parameters"]; + var results = new Array(); + if (components_paras.hasOwnProperty("common")) { + this.handle_common_components(component_name, components_paras["common"], results) + } + + if (results.length == 0 && components_paras.hasOwnProperty("role")) { + this.handle_role_components(component_name, components_paras["role"], results) + } + + return results + } + + print_parameter(parameters: any) { + var parameters_str = "" + parameters.forEach((onepara: any) => { + var str = "
" + for (var key in onepara) { + if (key == "role") { + str += "Role: " + onepara[key] + } else { + str += "
" + key + "
" + str += "
" + JSON.stringify(onepara[key]) + "
" + } + } + str += "
" + parameters_str += str + }); + return parameters_str; + } + + Generate() { + this.g = new dagreD3.graphlib.Graph(); + this.g.setGraph({ + rankdir: this.rankdir + }); + + for (var component_name in this.dsl) { + // get algorithm + var one_model = this.dsl[component_name] + this.g.setNode(component_name, { + label: component_name, + rx: 5, + ry: 5, + width: 130, + shape: this.node_shape, + style: this.node_stype, + x: '50%', + y: '50%' + }); + + } + for (var component_name in this.dsl) { + var one_model = this.dsl[component_name]; + // decide if the node is root + if (one_model.conditions.hasOwnProperty("input")) { + var inputs = this.get_source_data(one_model.conditions["input"]) + inputs.forEach((one_input: any) => { + var source_component = one_input.split(".")[0]; + const result = Object.keys(this.dsl).find(el => el === source_component) + if (result) { + this.getComponentRelation(component_name, inputs[0].split(".")[0]) + this.g.setEdge(source_component, component_name, { + label: "", + style: this.edge_stype + }); + } + }); + } else { + this.checkboxFirst = component_name + } + } + this.my[0] = { + name: 'reader_0', + value: false + } + this.level = 0 + this.count = 0 + this.mapCheckbox(this.checkboxFirst) + + } + checkboxFirst = '' + my: any[] = [] + level = 0 + count = 0 + Draw() { + // clear board + d3.selectAll("#svg-canvas-drop > *").remove(); + + let render = new dagreD3.render(); + // getsvg + let svg = this.d3.select(this.canva_selector); + let svgGroup: any = svg.append('g').attr('transform', 'translate(310,0)scale(' + this.zoomMultiples + ')') + render(svgGroup, this.g); + let tooltip = this.d3.select("body").append("div").classed(this.tooltip_css, true).style("opacity", 0).style("display", "none"); + svg.selectAll("g.node").on('click', (name: string) => { + this.attrForm.moduleName = '' + this.attrForm.attrList = [] + this.attrForm.diffList = [] + for (const key in this.dsl) { + if (key === name) { + this.attrForm.moduleName = name + for (const attr in this.dsl[key].attributes) { + if (Object.prototype.toString.call(this.dsl[key].attributes[attr]) === '[object Object]' + || Object.prototype.toString.call(this.dsl[key].attributes[attr]) === '[object Array]') { + if (this.dsl[key].attributes[attr].hasOwnProperty('drop_down_box')) { + + this.attrForm.attrList.push({ + name: attr, + value: this.dsl[key].attributes[attr] + }) + } else { + this.attrForm.attrList.push({ + name: attr, + value: JSON.stringify(this.dsl[key].attributes[attr]) + }) + } + } else { + this.attrForm.attrList.push({ + name: attr, + value: this.dsl[key].attributes[attr], + }) + } + } + if (this.dsl[key].attributeType === 'common') { + this.attrForm.options = 'common' + } else { + this.attrForm.options = 'diff' + for (const attr in this.dsl[key].diffAttribute) { + this.attrForm.diffList.push({ + name: attr, + form: this.dsl[key].diffAttribute[attr] + }) + } + } + } + } + this.dagModify(this.attrForm, this.that) + }) + svg.selectAll("g.node").on('dblclick', (name: string) => { + this.that.dragObj = this.dsl[name] + this.that.bulletFrame(false, name) + }) + svg.selectAll("g.node").append('image').attr('xlink:href', '../assets/close.jpg').attr('width', 10).attr('fill', 'none').attr('transform', 'translate(75,-17)') + svg.selectAll("g.node image").on("click", (e: any, a: any, c: any) => { + this.g.removeNode(e) + this.g.removeEdge(e) + delete this.dsl[e] + this.cObj = {} + this.my.forEach((el, index) => { + if (el.name === e) { + this.my.splice(index, 1) + this.that.program.splice(index, 1) + } + }) + render(svgGroup, this.g); + }) + svg.selectAll("g.edgePath").on("mouseover", (e: any) => { + }); + svg.attr("preserveAspectRatio", "xMidYMin") + if (this.count > 3 && this.level < 11) { + svg.attr("viewBox", "0 0 " + ((this.count - 3) * 200 + 900) + " 783") + } else if (this.count > 3 && this.level > 10) { + svg.attr("viewBox", "0 0 " + ((this.count - 3) * 200 + 900) + " " + ((this.level - 10) * 90 + 783)) + } else if (this.count < 3 && this.level > 10) { + svg.attr("viewBox", "0 0 900 " + ((this.level - 10) * 90 + 783)) + } else { + svg.attr("viewBox", "0 0 900 783") + } + } + cObj: any = {} + getComponentRelation(child: string, component_name: string) { + if (this.cObj.hasOwnProperty(component_name)) { + if (!this.cObj[component_name].children.find((el: string) => el === child)) { + this.cObj[component_name].children.push(child) + } + if (!this.cObj.hasOwnProperty(child)) { + this.cObj[child] = { + name: child, + children: [] + } + } + } else { + this.cObj[component_name] = { + name: component_name, + children: [child] + } + } + } + mapCheckbox(name: string): any { + if (!this.cObj[name] || !(this.cObj[name].children.length > 0)) { + this.level++ + if (this.find(name)) { + this.my.push({ + name: name, + value: false, + }) + } + this.checkboxFirst = name + return false + } else if (this.cObj[name].children.length < 2) { + this.level++ + if (this.find(this.cObj[name].children[0])) { + this.my.push({ + name: this.cObj[name].children[0], + value: false + }) + } + this.checkboxFirst = this.cObj[name].children[0] + return this.mapCheckbox(this.checkboxFirst) + } else if (this.cObj[name].children.length > 2) { + this.level++ + if (this.cObj[name].children.length > this.count) this.count = this.cObj[name].children.length + this.cObj[name].children.forEach((el: string) => { + if (this.find(el)) { + this.my.push({ + name: el, + value: false + }) + } + }); + this.cObj[name].children.forEach((el: string) => { + this.checkboxFirst = el + return this.mapCheckbox(this.checkboxFirst) + }); + } + } + find(item: any) { + return this.my.findIndex(c => c.name === item) === -1 ? true : false + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/config/dag.ts b/site-portal/frontend/src/config/dag.ts new file mode 100644 index 00000000..06cd5a68 --- /dev/null +++ b/site-portal/frontend/src/config/dag.ts @@ -0,0 +1,307 @@ +import * as dagreD3 from 'dagre-d3' +import * as d3 from 'd3' +interface CheckboxModel { + [key: string]: string +} +export default class Dag { + g: any; + node_shape = "rect"; + node_stype = "fill:#fff;stroke:#000;"; + edge_stype = "fill:#fff;stroke:#333;stroke-width:1.5px"; + info_bg = "#000"; + info_font = "#fff"; + info_font_size = "0.7em" + rankdir = "TB"; + tooltip_css = '' + dsl: any + conf: any + canva_selector: any + info_selector: any + d3 = d3 as any + checkboxArray: any[] = [] + checkboxFirst = '' + constructor(dsl: string, conf: any, canva_selector: any, info_selector: any = null) { + this.dsl = typeof dsl == 'string' ? JSON.parse(dsl) : dsl; + this.conf = typeof conf == 'string' ? JSON.parse(conf) : conf; + this.canva_selector = canva_selector + this.info_selector = info_selector + } + + get_source_data(input_filed: any): any { + var source_inputs = new Array(); + for (var key in input_filed) { + if (!Array.isArray(input_filed[key])) { + return this.get_source_data(input_filed[key]); + } else { + input_filed[key].forEach((val: string) => { + source_inputs.push(val); + }); + } + } + return source_inputs + } + + handle_common_components(component_name: string, common_components: any, results: any) { + var iter = function (obj: any, results: any[]) { + for (var key in obj) { + if (key == component_name) { + results.push(obj[key]) + return results; + } else { + if (typeof obj[key] == "object") { + iter(obj[key], results); + } else { + return results; + } + } + } + + return results + } + + return iter(common_components, results) + } + + handle_role_components(component_name: string, role_components: any, results: any) { + var found = false + var iter = function (obj: any, results: any[]) { + for (var key in obj) { + if (key == component_name) { + found = true + results.push(obj[key]) + return results; + } else { + if (key == "guest" || key == "host") { + results.push({ + "role": key + }) + } + + if (typeof obj[key] == "object") { + iter(obj[key], results); + } else { + return results; + } + } + } + if (!found) { + results.pop() + } + + return results + } + + return iter(role_components, results) + } + + get_component_para(component_name: string) { + var components_paras = this.conf["component_parameters"]; + var results = new Array(); + if (components_paras.hasOwnProperty("common")) { + this.handle_common_components(component_name, components_paras["common"], results) + } + + if (results.length == 0 && components_paras.hasOwnProperty("role")) { + this.handle_role_components(component_name, components_paras["role"], results) + } + + return results + } + + reset_canvas(canvas: any) { + canvas.selectAll("*").remove(); + } + + print_parameter(parameters: any) { + var parameters_str = '' + if (this.tooltip_css === 'dagTips') { + parameters_str = ` +
+ + + + + + ` + } else { + parameters_str = `
keyvalue
+ + + + + + ` + } + parameters.forEach((onepara: any) => { + var str = '' + for (var key in onepara) { + if (key == "role") { + str += ` + ` + str += "" + } else { + str += '' + str += "" + str += "" + str += '' + } + } + parameters_str += str + }); + return parameters_str + '
keyvalue
Role${onepara[key]}
" + key + "" + JSON.stringify(onepara[key]) + "
'; + } + + Generate() { + this.g = new dagreD3.graphlib.Graph(); + this.g.setGraph({ + rankdir: this.rankdir + }); + for (let component_name in this.dsl.components) { + var one_model = this.dsl.components[component_name] + this.g.setNode(component_name, { + label: component_name, + rx: 5, + ry: 5, + shape: this.node_shape, + style: this.node_stype + }); + } + for (let component_name in this.dsl.components) { + var one_model = this.dsl.components[component_name]; + if (one_model.hasOwnProperty("input")) { + var inputs = this.get_source_data(one_model["input"]); + this.getComponentRelation(component_name, inputs[0].split(".")[0]) + inputs.forEach((one_input: string) => { + var source_component = one_input.split(".")[0]; + this.g.setEdge(source_component, component_name, { + label: "", + style: this.edge_stype + }); + }); + } else { + this.checkboxFirst = component_name + } + } + this.my[0] = { + name: this.checkboxFirst, + value: true + } + this.mapCheckbox(this.checkboxFirst) + } + + Draw() { + let render = new dagreD3.render(); + let svg = d3.select(this.canva_selector); + this.reset_canvas(svg) + let svgGroup: any = svg.append('g'); + render(svgGroup, this.g); + if (this.info_selector == null) { + let info = d3.select("body").append("div") + .style("position", "absolute") + .style("opacity", 0) + .style("background-color", this.info_bg) + .style("font-size", this.info_font_size) + .style("color", this.info_font) + .style("padding-left", "10px") + .style("padding-right", "10px") + .style("display", "none"); + + // mouseover for detail + svg.selectAll("g.node").on("mouseenter", (e: any) => { + info.transition() + .duration(400) + .style('opacity', 0.9) + .style('display', 'block') + .style('position', 'absolute'); + info.html(this.print_parameter(this.get_component_para(e)) + '
') + .style('left', (this.d3.event.pageX + 35) + 'px') + .style('top', (this.d3.event.pageY + 5) + 'px'); + }) + .on("mouseleave", e => { + info.transition() + .duration(400) + .style('opacity', 0) + .style('display', 'none'); + }); + } else { + let info = d3.select(this.info_selector); + svg.selectAll("g.node").on("mouseenter", (e: any) => { + info.html(this.print_parameter(this.get_component_para(e))); + }) + .on("mouseleave", e => { + info.html(""); + }); + + } + + svg.selectAll("g.edgePath").on("mouseenter", e => { + }); + + svg.attr("preserveAspectRatio", "xMidYMin").attr("viewBox", "0 0 800 1100") + + } + count = 0 + my: any[] = [] + cObj: any = {} + getComponentRelation(child: string, component_name: string) { + if (this.cObj.hasOwnProperty(component_name)) { + this.cObj[component_name].children.push(child) + } else { + this.cObj[component_name] = { + name: component_name, + children: [child] + } + } + } + mapCheckbox(name: string): any { + if (!this.cObj[name]) { + if (this.find(name)) { + this.my.push({ + name: name, + value: false + }) + } + this.checkboxFirst = name + return false + } else if (this.cObj[name].children.length < 2) { + if (this.find(this.cObj[name].children[0])) { + if (this.cObj[name].children[0].indexOf('evaluation') !== -1 || this.cObj[name].children[0].indexOf('homo_data_split_0') !== -1) { + this.my.push({ + name: this.cObj[name].children[0], + value: false + }) + } else { + this.my.push({ + name: this.cObj[name].children[0], + value: true + }) + } + } + this.checkboxFirst = this.cObj[name].children[0] + return this.mapCheckbox(this.checkboxFirst) + } else if (this.cObj[name].children.length > 2) { + this.cObj[name].children.forEach((el: string) => { + if (this.find(el)) { + if (el.indexOf('evaluation') !== -1 || el.indexOf('homo_data_split_0') !== -1) { + this.my.push({ + name: el, + value: false + }) + } else { + this.my.push({ + name: el, + value: false + }) + } + } + }); + this.cObj[name].children.forEach((el: string) => { + this.checkboxFirst = el + return this.mapCheckbox(this.checkboxFirst) + }); + } + } + find(item: any) { + return this.my.findIndex(c => c.name === item) === -1 ? true : false + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/config/validators.ts b/site-portal/frontend/src/config/validators.ts new file mode 100644 index 00000000..19404391 --- /dev/null +++ b/site-portal/frontend/src/config/validators.ts @@ -0,0 +1,267 @@ +import { Validators, AbstractControl, NG_VALIDATORS, FormGroup } from '@angular/forms'; + +function myIndexOf(str: string, condition: string): number { + const newStr = str.toLocaleLowerCase() + const newCondition = condition.toLocaleLowerCase() + return newStr.indexOf(newCondition) +} + +function maxOrMin(value: any, max: number, min: number, defaultValue = null as any, el: groupModel) { + let arr = [defaultValue, [EmptyValidator, value]] + if (el.type[0] === 'notRequired') { + if (value !== null && value !== 'notRequired') { + arr = [defaultValue, value] + } else { + arr = [defaultValue] + } + if (max && min) { + arr = [defaultValue, [value, Validators.maxLength(max), Validators.minLength(min)]] + } else if (max) { + arr = [defaultValue, [value, Validators.maxLength(max)]] + } else if (min) { + arr = [defaultValue, [value, Validators.minLength(min)]] + } + } else { + arr = [defaultValue, [EmptyValidator]] + if (max && min) { + arr = [defaultValue, [EmptyValidator, value, Validators.maxLength(max), Validators.minLength(min)]] + } else if (max) { + arr = [defaultValue, [EmptyValidator, value, Validators.maxLength(max)]] + } else if (min) { + arr = [defaultValue, [EmptyValidator, value, Validators.minLength(min)]] + } else { + if (value) { + arr = [defaultValue, [EmptyValidator, value]] + } + } + } + return arr +} +interface groupModel { + name: string + type: string[], + value?: any + max?: number + min?: number +} +export function ValidatorGroup(group: groupModel[]) { + const newGroup: any = {} + group.forEach(el => { + el.type.forEach(key => { + switch (key) { + case 'number': + newGroup[el.name] = maxOrMin(NumberValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'word': + newGroup[el.name] = maxOrMin(WordValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'noSpace': + newGroup[el.name] = maxOrMin(noSpaceAlphanumericValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'email': + newGroup[el.name] = maxOrMin(EmailValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'internet': + newGroup[el.name] = maxOrMin(InternetValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'ip': + newGroup[el.name] = maxOrMin(IpValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'zero': + newGroup[el.name] = maxOrMin(zeroToHundred, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'require': + newGroup[el.name] = maxOrMin('', el.max || 0, el.min || 0, el.value || null, el) + break; + case 'json': + newGroup[el.name] = maxOrMin(JsonValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'endpoint': + newGroup[el.name] = maxOrMin(endpointValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'endpointWithoutPort': + newGroup[el.name] = maxOrMin(endpointWithoutPortValidator, el.max || 0, el.min || 0, el.value || null, el) + break; + case 'notRequired': + newGroup[el.name] = maxOrMin('notRequired', el.max || 0, el.min || 0, el.value || null, el) + break; + default: + if (el.value) { + newGroup[el.name] = [el.value] + } else { + newGroup[el.name] = [null] + } + break; + } + }) + }) + + return newGroup +} +// empty +export function EmptyValidator(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (typeof v === 'string' && !v?.trim()) { + return { emptyMessage: 'validator.empty' } + } else if (!v) { + return { emptyMessage: 'validator.empty' } + } + return null +} + +// email +export function EmailValidator(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (myIndexOf(v, '@') === -1) { + return { message: 'validator.email' } + } + return null +} +// numbers +export function NumberValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[0-9]*$/ + const v = control.value + if (v === '' || v === null) { + return null + } + if (!reg.test(v)) { + return { message: 'validator.number' } + } + return null +} +// Chinese, English, numbers +export function WordValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[\u4E00-\u9FA5A-Za-z0-9\s\d\-_/]+$/ + const v = control.value + + if (!reg.test(v?.trim())) { + return { message: 'validator.word' } + } + return null +} +// Chinese, English, numbers +export function noSpaceAlphanumericValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[\u4E00-\u9FA5A-Za-z0-9\d\-_/]+$/ + const v = control.value + + if (!reg.test(v?.trim())) { + return { message: 'validator.noSpace' } + } + return null +} +// English, numbers +export function EgNuValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^[\u4E00-\u9FA5A-Za-z0-9]+$/ + const v = control.value + if (!reg.test(v?.trim())) { + return { message: 'validator.engu' } + } + return null +} +// internetUrl +export function InternetValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^(http|https):\/\/([\w]+)\S*/ + const v = control.value + if (v === '' || v === null) { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.internet' } + } + return null +} + +// ip +export function IpValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g + const v = control.value + if (v === '' || v === null || v.indexOf('localhost')==0) { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.ip' } + } + return null +} + +// 0-100 +export function zeroToHundred(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (v === '' || v === null) { + return null + } + + if (+v < 0 || +v > 100) { + return { message: 'validator.zeroToHundred' } + } + return null +} + + +// json +export function JsonValidator(control: AbstractControl): { [key: string]: any } | null { + const reg = /^\s*\{\s*[A-Z0-9._]+\s*:\s*[A-Z0-9._]+\s*(,\s*[A-Z0-9._]+\s*:\s*[A-Z0-9._]+\s*)*\}\s*$/i + const v = control.value + if (v === '' || v === null || v === '{}') { + return null + } + if (!reg.test(v?.trim())) { + return { message: 'validator.json' } + } + return null +} + +// endpoint: a valid FQDN or IP +export function endpointValidator(control: AbstractControl): { [key: string]: any } | null { + const fqdn_reg = /(?=^.{4,253}$)(^((?!-)[a-z0-9-]{0,62}[a-z0-9]\.)+[a-z]{2,63}$)/gm + const ip_reg = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g + const v = control.value + if (v === '' || v === null || v.indexOf('localhost')==0) { + return null + } + if (!ip_reg.test(v?.trim())) { + if (!fqdn_reg.test(v?.trim())) { + return { message: 'validator.endpoint' } + } + } + return null +} + +// endpoint address without port +export function endpointWithoutPortValidator(control: AbstractControl): { [key: string]: any } | null { + const v = control.value + if (v === '' || v === null ) { + return null + } + if (v.indexOf(':')>=0) { + return { message: 'validator.endpintWithoutPort' } + } + if (v.indexOf('localhost')===0) return null + const fqdn_reg = /(?=^.{4,253}$)(^((?!-)[a-z0-9-]{0,62}[a-z0-9]\.)+[a-z]{2,63}$)/gm + const ip_reg = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g + if (!ip_reg.test(v?.trim())) { + if (!fqdn_reg.test(v?.trim())) { + return { message: 'validator.endpoint' } + } + } + return null +} + +//change password matching +export function ConfirmedValidator(controlName: string, matchingControlName: string) { + + return (formGroup: FormGroup) => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + if (matchingControl.errors && !matchingControl.errors.confirmedValidator) { + return; + } + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ confirmedValidator: true }); + } else { + matchingControl.setErrors(null); + } + + } + +} diff --git a/site-portal/frontend/src/environments/environment.prod.ts b/site-portal/frontend/src/environments/environment.prod.ts new file mode 100644 index 00000000..3612073b --- /dev/null +++ b/site-portal/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/site-portal/frontend/src/environments/environment.ts b/site-portal/frontend/src/environments/environment.ts new file mode 100644 index 00000000..f56ff470 --- /dev/null +++ b/site-portal/frontend/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/site-portal/frontend/src/index.html b/site-portal/frontend/src/index.html new file mode 100644 index 00000000..d05e4beb --- /dev/null +++ b/site-portal/frontend/src/index.html @@ -0,0 +1,13 @@ + + + + + Site Portal + + + + + + + + diff --git a/site-portal/frontend/src/main.ts b/site-portal/frontend/src/main.ts new file mode 100644 index 00000000..c7b673cf --- /dev/null +++ b/site-portal/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/site-portal/frontend/src/polyfills.ts b/site-portal/frontend/src/polyfills.ts new file mode 100644 index 00000000..83dc6f48 --- /dev/null +++ b/site-portal/frontend/src/polyfills.ts @@ -0,0 +1,69 @@ +/*************************************************************************************************** + * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. + */ +import '@angular/localize/init'; +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/site-portal/frontend/src/styles.css b/site-portal/frontend/src/styles.css new file mode 100644 index 00000000..e69de29b diff --git a/site-portal/frontend/src/test.ts b/site-portal/frontend/src/test.ts new file mode 100644 index 00000000..20423564 --- /dev/null +++ b/site-portal/frontend/src/test.ts @@ -0,0 +1,25 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/site-portal/frontend/src/utils/auth-interceptor.ts b/site-portal/frontend/src/utils/auth-interceptor.ts new file mode 100644 index 00000000..9167da60 --- /dev/null +++ b/site-portal/frontend/src/utils/auth-interceptor.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Router } from '@angular/router' +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { MessageService } from '../app/components/message/message.service' +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(public router: Router, private $msg: MessageService) { + } + intercept(req: HttpRequest, next: HttpHandler): Observable> { + let newReq: HttpRequest + // get language package + if (req.url.indexOf('../assets') === -1) { + newReq = req.clone({ + url: '/api/v1' + req.url + }) + } else {// other request + newReq = req.clone() + } + return next.handle(newReq).pipe( + tap( + res => res, + (err: HttpErrorResponse) => { + if (err.status === 401 && this.router.url.indexOf('/login') === -1) { + sessionStorage.removeItem('portal-username') + sessionStorage.removeItem('sitePortal-redirect') + const url = this.router.url + if (newReq.url.indexOf('/user/current') !== -1) { + this.router.navigateByUrl(`/login`) + } else { + sessionStorage.setItem('sitePortal-redirect', url) + this.router.navigate(['/login/']) + } + } else if (err.status === 404) { + this.$msg.warning('serverMessage.default404') + } + return err + } + ) + ) + } + +} \ No newline at end of file diff --git a/site-portal/frontend/src/utils/comparator.ts b/site-portal/frontend/src/utils/comparator.ts new file mode 100644 index 00000000..7ee948c9 --- /dev/null +++ b/site-portal/frontend/src/utils/comparator.ts @@ -0,0 +1,58 @@ +import { ClrDatagridComparatorInterface } from '@clr/angular'; + +export class CustomComparator implements ClrDatagridComparatorInterface { + + fieldName: string; + type: string; + + constructor(fieldName: string, type: string) { + this.fieldName = fieldName; + this.type = type; + } + + compare(a: { [key: string]: any | any[] }, b: { [key: string]: any | any[] }) { + let comp = 0; + if (a && b) { + let fieldA, fieldB; + for (let key of Object.keys(a)) { + if (key === this.fieldName) { + fieldA = a[key]; + fieldB = b[key]; + break; + } else if (typeof a[key] === 'object') { + let insideObject = a[key]; + for (let insideKey in insideObject) { + if (insideKey === this.fieldName) { + fieldA = insideObject[insideKey]; + fieldB = b[key][insideKey]; + break; + } + } + } + } + switch (this.type) { + case "number": + comp = fieldB - fieldA; + break; + case "date": + comp = new Date(fieldB).getTime() - new Date(fieldA).getTime(); + break; + case "string": + comp = fieldB.localeCompare(fieldA); + break; + //project job management list defualt sorting + //firstly put "pending on this site" job on top, then sort others by creation time, if there are multiple "pending on this site" jobs, they should be sorted by creation time too + case "job": + if (a.pending_on_this_site && !b.pending_on_this_site) { + comp = -1 + } else if (!a.pending_on_this_site && b.pending_on_this_site) { + comp = 1 + } else { + comp = new Date(fieldB).getTime() - new Date(fieldA).getTime(); + } + break; + } + } + return comp; + } +} \ No newline at end of file diff --git a/site-portal/frontend/src/utils/compile.ts b/site-portal/frontend/src/utils/compile.ts new file mode 100644 index 00000000..f76650b0 --- /dev/null +++ b/site-portal/frontend/src/utils/compile.ts @@ -0,0 +1,17 @@ +export function compile(code: string) { + let newStr = String.fromCharCode(code.charCodeAt(0) + code.length) + for (let i = 1; i < code.length; i++) { + newStr += String.fromCharCode(code.charCodeAt(i) + code.length) + } + return escape(newStr) +} + +export function uncompile(code: string) { + code = unescape(code) + + let newStr = String.fromCharCode(code.charCodeAt(0) - code.length) + for (let i = 1; i < code.length; i++) { + newStr += String.fromCharCode(code.charCodeAt(i) - code.length) + } + return newStr +} \ No newline at end of file diff --git a/site-portal/frontend/src/utils/high-json.ts b/site-portal/frontend/src/utils/high-json.ts new file mode 100644 index 00000000..4eff1722 --- /dev/null +++ b/site-portal/frontend/src/utils/high-json.ts @@ -0,0 +1,93 @@ +export function isCollapsable(arg: any): boolean { + return arg instanceof Object && Object.keys(arg).length > 0; +} +export function isUrl(string: string): boolean { + var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i; + return urlRegexp.test(string); +} +export function json2html(json: string | any, options: any): string { + var html = ''; + if (typeof json === 'string') { + // Escape tags and quotes + json = json + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"'); + + if (options.withLinks && isUrl(json)) { + html += '' + json + ''; + } else { + // Escape double quotes in the rendered non-URL string. + json = json.replace(/"/g, '\\"'); + html += '"' + json + '"'; + } + } else if (typeof json === 'number') { + html += '' + json + ''; + } else if (typeof json === 'boolean') { + html += '' + json + ''; + } else if (json === null) { + html += 'null'; + } else if (json instanceof Array) { + if (json.length > 0) { + html += '[
    '; + for (var i = 0; i < json.length; ++i) { + html += '
  1. '; + // Add toggle button if item is collapsable + if (isCollapsable(json[i])) { + html += ''; + } + html += json2html(json[i], options); + // Add comma if item is not last + if (i < json.length - 1) { + html += ','; + } + html += '
  2. '; + } + html += '
]'; + } else { + html += '[]'; + } + } else if (typeof json === 'object') { + var keyCount = Object.keys(json).length; + if (keyCount > 0) { + html += '{
    '; + for (var key in json) { + if (Object.prototype.hasOwnProperty.call(json, key)) { + html += '
  • '; + var keyRepr = options.withQuotes ? + '"' + key + '"' : key; + // Add toggle button if item is collapsable + if (isCollapsable(json[key])) { + html += '' + keyRepr + ''; + } else { + html += keyRepr; + } + html += ': ' + json2html(json[key], options); + // Add comma if item is not last + if (--keyCount > 0) { + html += ','; + } + html += '
  • '; + } + } + html += '
}'; + } else { + html += '{}'; + } + } + return html; +} +export function valueSplice(arr: string[], obj: any) { + for (const key in obj) { + if (key === arr[0]) { + obj[key] = arr[1] + break + } + if (Object.prototype.toString.call(obj[key]).slice(8) === 'Object]') { + const newObj = obj[key] + valueSplice(arr, newObj) + } + } +} diff --git a/site-portal/frontend/src/utils/selective-preloading-strategy.ts b/site-portal/frontend/src/utils/selective-preloading-strategy.ts new file mode 100644 index 00000000..e2b6ecb7 --- /dev/null +++ b/site-portal/frontend/src/utils/selective-preloading-strategy.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core' +import { PreloadingStrategy, Route } from '@angular/router' +import { Observable } from 'rxjs'; +import { of } from 'rxjs' +@Injectable() +export class SelectivePreloadingStrategy implements PreloadingStrategy { + preload(route: Route, fn: () => Observable): Observable { + if (route.data && route.data.preload) { + return fn() + } else { + return of(null) + } + } + +} \ No newline at end of file diff --git a/site-portal/frontend/tsconfig.app.json b/site-portal/frontend/tsconfig.app.json new file mode 100644 index 00000000..82d91dc4 --- /dev/null +++ b/site-portal/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/site-portal/frontend/tsconfig.json b/site-portal/frontend/tsconfig.json new file mode 100644 index 00000000..632478ea --- /dev/null +++ b/site-portal/frontend/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "module": "es2020", + "lib": [ + "es2018", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "strictNullChecks": false + } +} diff --git a/site-portal/frontend/tsconfig.spec.json b/site-portal/frontend/tsconfig.spec.json new file mode 100644 index 00000000..092345b0 --- /dev/null +++ b/site-portal/frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/site-portal/frontend/yarn.lock b/site-portal/frontend/yarn.lock new file mode 100644 index 00000000..d3c55194 --- /dev/null +++ b/site-portal/frontend/yarn.lock @@ -0,0 +1,9754 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@angular-devkit/architect@0.1200.5": + "integrity" "sha512-222VZ4OeaDK3vON8V5m+w15SRWfUs5uOb4H9ij/H9/6tyHD83uWfCDoOGg+ax4wJVdWEFJIS+Vn4ijGcZCq9WQ==" + "resolved" "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1200.5.tgz" + "version" "0.1200.5" + dependencies: + "@angular-devkit/core" "12.0.5" + "rxjs" "6.6.7" + +"@angular-devkit/build-angular@~12.0.5": + "integrity" "sha512-rFbaAQPeuWM9KE9lK3J0sF6GB9nKF/s2Z7rtzKux7whGTF3Tlj8NHrcSxZTf4eBm3cnEpaiod1uPBQg8fUWD4w==" + "resolved" "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@angular-devkit/architect" "0.1200.5" + "@angular-devkit/build-optimizer" "0.1200.5" + "@angular-devkit/build-webpack" "0.1200.5" + "@angular-devkit/core" "12.0.5" + "@babel/core" "7.14.3" + "@babel/generator" "7.14.3" + "@babel/plugin-transform-async-to-generator" "7.13.0" + "@babel/plugin-transform-runtime" "7.14.3" + "@babel/preset-env" "7.14.2" + "@babel/runtime" "7.14.0" + "@babel/template" "7.12.13" + "@discoveryjs/json-ext" "0.5.2" + "@jsdevtools/coverage-istanbul-loader" "3.0.5" + "@ngtools/webpack" "12.0.5" + "ansi-colors" "4.1.1" + "babel-loader" "8.2.2" + "browserslist" "^4.9.1" + "cacache" "15.0.6" + "caniuse-lite" "^1.0.30001032" + "circular-dependency-plugin" "5.2.2" + "copy-webpack-plugin" "8.1.1" + "core-js" "3.12.0" + "critters" "0.0.10" + "css-loader" "5.2.4" + "css-minimizer-webpack-plugin" "3.0.0" + "find-cache-dir" "3.3.1" + "glob" "7.1.7" + "https-proxy-agent" "5.0.0" + "inquirer" "8.0.0" + "jest-worker" "26.6.2" + "karma-source-map-support" "1.4.0" + "less" "4.1.1" + "less-loader" "8.1.1" + "license-webpack-plugin" "2.3.19" + "loader-utils" "2.0.0" + "mini-css-extract-plugin" "1.5.1" + "minimatch" "3.0.4" + "open" "8.0.2" + "ora" "5.4.0" + "parse5-html-rewriting-stream" "6.0.1" + "postcss" "8.3.0" + "postcss-import" "14.0.1" + "postcss-loader" "5.2.0" + "postcss-preset-env" "6.7.0" + "raw-loader" "4.0.2" + "regenerator-runtime" "0.13.7" + "resolve-url-loader" "4.0.0" + "rimraf" "3.0.2" + "rxjs" "6.6.7" + "sass" "1.32.12" + "sass-loader" "11.0.1" + "semver" "7.3.5" + "source-map" "0.7.3" + "source-map-loader" "2.0.1" + "source-map-support" "0.5.19" + "style-loader" "2.0.0" + "stylus" "0.54.8" + "stylus-loader" "5.0.0" + "terser" "5.7.0" + "terser-webpack-plugin" "5.1.2" + "text-table" "0.2.0" + "tree-kill" "1.2.2" + "webpack" "5.39.1" + "webpack-dev-middleware" "4.1.0" + "webpack-dev-server" "3.11.2" + "webpack-merge" "5.7.3" + "webpack-subresource-integrity" "1.5.2" + +"@angular-devkit/build-optimizer@0.1200.5": + "integrity" "sha512-3XlDVVak3CfIgUjDZMoON7sxnI1vxhzEm2LvVg5yN98Q7ijnfykXiIzryEcplzTMTZwGNkem0361HDs1EX8zNg==" + "resolved" "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1200.5.tgz" + "version" "0.1200.5" + dependencies: + "source-map" "0.7.3" + "tslib" "2.2.0" + "typescript" "4.2.4" + +"@angular-devkit/build-webpack@0.1200.5": + "integrity" "sha512-cx8DVwOmogzeHS1dsZGWummwA3T1zqsJ6q9NIL0S0ZiOScZuz0j9zAOLjBkyCj5XexOFzHv3TcAVQyzJsi1tIw==" + "resolved" "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1200.5.tgz" + "version" "0.1200.5" + dependencies: + "@angular-devkit/architect" "0.1200.5" + "rxjs" "6.6.7" + +"@angular-devkit/core@12.0.5": + "integrity" "sha512-zVSQV+8/vjUjsUKGlj8Kf5LioA6AXJTGI0yhHW9q1dFX4dPpbW63k0R1UoIB2wJ0F/AbYVgpnPGPe9BBm2fvZA==" + "resolved" "https://registry.npmjs.org/@angular-devkit/core/-/core-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "ajv" "8.2.0" + "ajv-formats" "2.0.2" + "fast-json-stable-stringify" "2.1.0" + "magic-string" "0.25.7" + "rxjs" "6.6.7" + "source-map" "0.7.3" + +"@angular-devkit/schematics@12.0.5": + "integrity" "sha512-iW3XuDHScr3TXuunlEjF5O01zBpwpLgfr1oEny8PvseFGDlHK4Nj8zNIoIn3Yg936aiFO4GJAC/UXsT8g5vKxQ==" + "resolved" "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@angular-devkit/core" "12.0.5" + "ora" "5.4.0" + "rxjs" "6.6.7" + +"@angular/animations@^12.0.0 || ^13.0.0-0", "@angular/animations@~12.0.5", "@angular/animations@12.0.5": + "integrity" "sha512-BPdTCtgDJ9zNzHpuA6X3NmtzDiIt5SHZk840j0q3HCq6rP6C/oo2UnPT6w8YDOGMejDpWdHvTgXH4jVKDN1wCQ==" + "resolved" "https://registry.npmjs.org/@angular/animations/-/animations-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/cdk@^12.1.1", "@angular/cdk@12.1.1": + "integrity" "sha512-MJENa8qmfLAr6t59u1+mEC2YPbCn4n3vsY6k8fKyf+ILXwwGHWNZlYblaRMBjrF/crSx1Kd5vb30RCqIcNTGsA==" + "resolved" "https://registry.npmjs.org/@angular/cdk/-/cdk-12.1.1.tgz" + "version" "12.1.1" + dependencies: + "tslib" "^2.2.0" + optionalDependencies: + "parse5" "^5.0.0" + +"@angular/cli@~12.0.5": + "integrity" "sha512-MdgJ9DY3bWYsMFr9Xa+60gtVaYErhAE8ULGnUyI8zLMhWqrV1ZpJJ1WfP8pMQYx4HaKJIgx7wd8az7ZXAcF8hg==" + "resolved" "https://registry.npmjs.org/@angular/cli/-/cli-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@angular-devkit/architect" "0.1200.5" + "@angular-devkit/core" "12.0.5" + "@angular-devkit/schematics" "12.0.5" + "@schematics/angular" "12.0.5" + "@yarnpkg/lockfile" "1.1.0" + "ansi-colors" "4.1.1" + "debug" "4.3.1" + "ini" "2.0.0" + "inquirer" "8.0.0" + "jsonc-parser" "3.0.0" + "npm-package-arg" "8.1.2" + "npm-pick-manifest" "6.1.1" + "open" "8.0.2" + "ora" "5.4.0" + "pacote" "11.3.2" + "resolve" "1.20.0" + "rimraf" "3.0.2" + "semver" "7.3.5" + "symbol-observable" "4.0.0" + "uuid" "8.3.2" + +"@angular/common@^11.0.0", "@angular/common@^12.0.0", "@angular/common@^12.0.0 || ^13.0.0-0", "@angular/common@>=10.0.0", "@angular/common@~12.0.5", "@angular/common@12.0.5": + "integrity" "sha512-jKiPjWVL3jXVKgXwINlsF5O0r9gX/mAoa5UVy57O8jcg+ENbH9LLSOikgiF/0HPxk2uvRV5OYmbBgOY1xT41kQ==" + "resolved" "https://registry.npmjs.org/@angular/common/-/common-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/compiler-cli@^12.0.0", "@angular/compiler-cli@~12.0.5", "@angular/compiler-cli@12.0.5": + "integrity" "sha512-XBZWU2S7N2kvQJK0H5KyLHiLVhYJrjh3NtbVBv67sCY9Ft8fv2jjbozTgXqeoYZ1xAxcZ2ZAB0n5SkhmY75Mow==" + "resolved" "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@babel/core" "^7.8.6" + "@babel/types" "^7.8.6" + "canonical-path" "1.0.0" + "chokidar" "^3.0.0" + "convert-source-map" "^1.5.1" + "dependency-graph" "^0.11.0" + "magic-string" "^0.25.0" + "minimist" "^1.2.0" + "reflect-metadata" "^0.1.2" + "semver" "^7.0.0" + "source-map" "^0.6.1" + "sourcemap-codec" "^1.4.8" + "tslib" "^2.1.0" + "yargs" "^16.2.0" + +"@angular/compiler@~12.0.5", "@angular/compiler@12.0.5": + "integrity" "sha512-G255aP4hhQ04hSQ1/JgiKNeTzRCuLgK220lJXkHubpu+f67os5LArhFNxZaUC/EVa/sloOT7Fo5tn8C5gy7iLA==" + "resolved" "https://registry.npmjs.org/@angular/compiler/-/compiler-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/core@^11.0.0", "@angular/core@^12.0.0", "@angular/core@^12.0.0 || ^13.0.0-0", "@angular/core@>=10.0.0", "@angular/core@~12.0.5", "@angular/core@12.0.5": + "integrity" "sha512-uVYsZa1VqVw8vNcjUYgfjXBc1M3WaxLXoLnCsDvutJiln4csa8Yw8p7RqQSrZ6AgQ8o2LHD2JJ5kus3EDMwfcA==" + "resolved" "https://registry.npmjs.org/@angular/core/-/core-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/forms@^12.0.0", "@angular/forms@^12.0.0 || ^13.0.0-0", "@angular/forms@~12.0.5": + "integrity" "sha512-Ew/fGPTsywoYnm6DFPA/DyLl4Sb+1/uzpledrbxUHzaSKIrnXFrjQiUTmsbbq+8qono3JzbUIblqH1DrNThYiA==" + "resolved" "https://registry.npmjs.org/@angular/forms/-/forms-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/localize@^12.0.0", "@angular/localize@~12.0.5": + "integrity" "sha512-CRyXsNJYV7TJBsbG/Sn6lW9qMQCa+lw5SSNKHvnmfCTyd5p3DV8AdjOYkyWM5tfB4wg/aOc4C1ynDom4TmKv+w==" + "resolved" "https://registry.npmjs.org/@angular/localize/-/localize-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@babel/core" "7.8.3" + "glob" "7.1.7" + "yargs" "^16.2.0" + +"@angular/material@^12.1.1": + "integrity" "sha512-q2vhbVQfjr+88beqCotCVlaPRyy9y2O2asiR3+0BU9OJ1DjTo1QpBmMpck5InP7Es49t9RTk9RzzC0t18p45yA==" + "resolved" "https://registry.npmjs.org/@angular/material/-/material-12.1.1.tgz" + "version" "12.1.1" + dependencies: + "tslib" "^2.2.0" + +"@angular/platform-browser-dynamic@~12.0.5": + "integrity" "sha512-sYkOJxXj4vEZICT2oODkQF9wNaKoScSkiw2ooBYN0UX02mFKlWKa9vkzp6JmN1EF8YOWF0JnRqBPAi1WbOnAMw==" + "resolved" "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/platform-browser@~12.0.5", "@angular/platform-browser@12.0.5": + "integrity" "sha512-MLioK9gcdZOKINE8OmIHZICRnFySaWyCBeVbHS4Z4Vxgr+E2S2eO1IxBmGyQpJq1wDPBP9A/9LVLMUNbHu4Cdw==" + "resolved" "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@angular/router@~12.0.5": + "integrity" "sha512-2jiaT+OxCmJbeJ0MTPmIHBsTFLysenvPZteozYsjcmUo9mOzJHAjqHLJvTC+Ri+E9xvnplh+8BPETRleV1pAFw==" + "resolved" "https://registry.npmjs.org/@angular/router/-/router-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "tslib" "^2.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.8.3": + "integrity" "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==" + "resolved" "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.0", "@babel/compat-data@^7.14.5", "@babel/compat-data@^7.14.7": + "integrity" "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==" + "resolved" "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz" + "version" "7.14.7" + +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.4.0-0", "@babel/core@^7.7.5", "@babel/core@^7.8.6", "@babel/core@7.14.3": + "integrity" "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==" + "resolved" "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz" + "version" "7.14.3" + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.14.3" + "@babel/helper-compilation-targets" "^7.13.16" + "@babel/helper-module-transforms" "^7.14.2" + "@babel/helpers" "^7.14.0" + "@babel/parser" "^7.14.3" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.2" + "@babel/types" "^7.14.2" + "convert-source-map" "^1.7.0" + "debug" "^4.1.0" + "gensync" "^1.0.0-beta.2" + "json5" "^2.1.2" + "semver" "^6.3.0" + "source-map" "^0.5.0" + +"@babel/core@7.8.3": + "integrity" "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==" + "resolved" "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.3" + "@babel/helpers" "^7.8.3" + "@babel/parser" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" + "convert-source-map" "^1.7.0" + "debug" "^4.1.0" + "gensync" "^1.0.0-beta.1" + "json5" "^2.1.0" + "lodash" "^4.17.13" + "resolve" "^1.3.2" + "semver" "^5.4.1" + "source-map" "^0.5.0" + +"@babel/generator@^7.14.3", "@babel/generator@^7.8.3", "@babel/generator@7.14.3": + "integrity" "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==" + "resolved" "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz" + "version" "7.14.3" + dependencies: + "@babel/types" "^7.14.2" + "jsesc" "^2.5.1" + "source-map" "^0.5.0" + +"@babel/generator@^7.14.5": + "integrity" "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==" + "resolved" "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + "jsesc" "^2.5.1" + "source-map" "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.14.5": + "integrity" "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==" + "resolved" "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": + "integrity" "sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==" + "resolved" "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-explode-assignable-expression" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.16", "@babel/helper-compilation-targets@^7.14.5": + "integrity" "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==" + "resolved" "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/compat-data" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "browserslist" "^4.16.6" + "semver" "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.14.5": + "integrity" "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==" + "resolved" "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz" + "version" "7.14.6" + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-member-expression-to-functions" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + +"@babel/helper-create-regexp-features-plugin@^7.14.5": + "integrity" "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==" + "resolved" "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "regexpu-core" "^4.7.1" + +"@babel/helper-define-polyfill-provider@^0.2.2": + "integrity" "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==" + "resolved" "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz" + "version" "0.2.3" + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + "debug" "^4.1.1" + "lodash.debounce" "^4.0.8" + "resolve" "^1.14.2" + "semver" "^6.1.2" + +"@babel/helper-explode-assignable-expression@^7.14.5": + "integrity" "sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-function-name@^7.14.5": + "integrity" "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-get-function-arity" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-get-function-arity@^7.14.5": + "integrity" "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==" + "resolved" "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-hoist-variables@^7.14.5": + "integrity" "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-member-expression-to-functions@^7.14.5": + "integrity" "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==" + "resolved" "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12", "@babel/helper-module-imports@^7.14.5": + "integrity" "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-transforms@^7.14.2", "@babel/helper-module-transforms@^7.14.5": + "integrity" "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==" + "resolved" "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-simple-access" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-optimise-call-expression@^7.14.5": + "integrity" "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==" + "resolved" "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + "integrity" "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz" + "version" "7.14.5" + +"@babel/helper-remap-async-to-generator@^7.13.0", "@babel/helper-remap-async-to-generator@^7.14.5": + "integrity" "sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==" + "resolved" "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-wrap-function" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-replace-supers@^7.14.5": + "integrity" "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==" + "resolved" "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-member-expression-to-functions" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-simple-access@^7.14.5": + "integrity" "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==" + "resolved" "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": + "integrity" "sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-split-export-declaration@^7.14.5": + "integrity" "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==" + "resolved" "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5": + "integrity" "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==" + "resolved" "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz" + "version" "7.14.5" + +"@babel/helper-validator-option@^7.12.17", "@babel/helper-validator-option@^7.14.5": + "integrity" "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==" + "resolved" "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz" + "version" "7.14.5" + +"@babel/helper-wrap-function@^7.14.5": + "integrity" "sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==" + "resolved" "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-function-name" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helpers@^7.14.0", "@babel/helpers@^7.8.3": + "integrity" "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==" + "resolved" "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz" + "version" "7.14.6" + dependencies: + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/highlight@^7.14.5": + "integrity" "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==" + "resolved" "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + "chalk" "^2.0.0" + "js-tokens" "^4.0.0" + +"@babel/parser@^7.12.13", "@babel/parser@^7.14.3", "@babel/parser@^7.14.5", "@babel/parser@^7.14.7", "@babel/parser@^7.8.3": + "integrity" "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + "resolved" "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz" + "version" "7.14.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": + "integrity" "sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + +"@babel/plugin-proposal-async-generator-functions@^7.14.2": + "integrity" "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.13.0": + "integrity" "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-class-static-block@^7.13.11": + "integrity" "sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.14.2": + "integrity" "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.14.2": + "integrity" "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.14.2": + "integrity" "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.14.2": + "integrity" "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.2": + "integrity" "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.14.2": + "integrity" "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.14.2": + "integrity" "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/compat-data" "^7.14.7" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.14.5" + +"@babel/plugin-proposal-optional-catch-binding@^7.14.2": + "integrity" "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.14.2", "@babel/plugin-proposal-optional-chaining@^7.14.5": + "integrity" "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.13.0": + "integrity" "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-proposal-private-property-in-object@^7.14.0": + "integrity" "sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.12.13", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + "integrity" "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-async-generators@^7.8.4": + "integrity" "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + "version" "7.8.4" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + "integrity" "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + "version" "7.12.13" + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.12.13", "@babel/plugin-syntax-class-static-block@^7.14.5": + "integrity" "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + "integrity" "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + "integrity" "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-json-strings@^7.8.3": + "integrity" "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + "integrity" "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + "version" "7.10.4" + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + "integrity" "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + "integrity" "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + "version" "7.10.4" + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + "integrity" "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + "integrity" "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + "integrity" "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + "version" "7.8.3" + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.0", "@babel/plugin-syntax-private-property-in-object@^7.14.5": + "integrity" "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.12.13": + "integrity" "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.13.0": + "integrity" "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-async-to-generator@^7.13.0", "@babel/plugin-transform-async-to-generator@7.13.0": + "integrity" "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz" + "version" "7.13.0" + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-remap-async-to-generator" "^7.13.0" + +"@babel/plugin-transform-block-scoped-functions@^7.12.13": + "integrity" "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-block-scoping@^7.14.2": + "integrity" "sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-classes@^7.14.2": + "integrity" "sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-annotate-as-pure" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "globals" "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.13.0": + "integrity" "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-destructuring@^7.13.17": + "integrity" "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-dotall-regex@^7.12.13", "@babel/plugin-transform-dotall-regex@^7.4.4": + "integrity" "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-duplicate-keys@^7.12.13": + "integrity" "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-exponentiation-operator@^7.12.13": + "integrity" "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-for-of@^7.13.0": + "integrity" "sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-function-name@^7.12.13": + "integrity" "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-literals@^7.12.13": + "integrity" "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-member-expression-literals@^7.12.13": + "integrity" "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-modules-amd@^7.14.2": + "integrity" "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "babel-plugin-dynamic-import-node" "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.14.0": + "integrity" "sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-simple-access" "^7.14.5" + "babel-plugin-dynamic-import-node" "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.13.8": + "integrity" "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.5" + "babel-plugin-dynamic-import-node" "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.14.0": + "integrity" "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-module-transforms" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.12.13": + "integrity" "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + +"@babel/plugin-transform-new-target@^7.12.13": + "integrity" "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-object-super@^7.12.13": + "integrity" "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.14.5" + +"@babel/plugin-transform-parameters@^7.14.2", "@babel/plugin-transform-parameters@^7.14.5": + "integrity" "sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.12.13": + "integrity" "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-regenerator@^7.13.15": + "integrity" "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "regenerator-transform" "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.12.13": + "integrity" "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-runtime@7.14.3": + "integrity" "sha512-t960xbi8wpTFE623ef7sd+UpEC5T6EEguQlTBJDEO05+XwnIWVfuqLw/vdLWY6IdFmtZE+65CZAfByT39zRpkg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.3.tgz" + "version" "7.14.3" + dependencies: + "@babel/helper-module-imports" "^7.13.12" + "@babel/helper-plugin-utils" "^7.13.0" + "babel-plugin-polyfill-corejs2" "^0.2.0" + "babel-plugin-polyfill-corejs3" "^0.2.0" + "babel-plugin-polyfill-regenerator" "^0.2.0" + "semver" "^6.3.0" + +"@babel/plugin-transform-shorthand-properties@^7.12.13": + "integrity" "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-spread@^7.13.0": + "integrity" "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz" + "version" "7.14.6" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + +"@babel/plugin-transform-sticky-regex@^7.12.13": + "integrity" "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-template-literals@^7.13.0": + "integrity" "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-typeof-symbol@^7.12.13": + "integrity" "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-escapes@^7.12.13": + "integrity" "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-regex@^7.12.13": + "integrity" "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==" + "resolved" "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/preset-env@7.14.2": + "integrity" "sha512-7dD7lVT8GMrE73v4lvDEb85cgcQhdES91BSD7jS/xjC6QY8PnRhux35ac+GCpbiRhp8crexBvZZqnaL6VrY8TQ==" + "resolved" "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.2.tgz" + "version" "7.14.2" + dependencies: + "@babel/compat-data" "^7.14.0" + "@babel/helper-compilation-targets" "^7.13.16" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-validator-option" "^7.12.17" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12" + "@babel/plugin-proposal-async-generator-functions" "^7.14.2" + "@babel/plugin-proposal-class-properties" "^7.13.0" + "@babel/plugin-proposal-class-static-block" "^7.13.11" + "@babel/plugin-proposal-dynamic-import" "^7.14.2" + "@babel/plugin-proposal-export-namespace-from" "^7.14.2" + "@babel/plugin-proposal-json-strings" "^7.14.2" + "@babel/plugin-proposal-logical-assignment-operators" "^7.14.2" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.2" + "@babel/plugin-proposal-numeric-separator" "^7.14.2" + "@babel/plugin-proposal-object-rest-spread" "^7.14.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.14.2" + "@babel/plugin-proposal-optional-chaining" "^7.14.2" + "@babel/plugin-proposal-private-methods" "^7.13.0" + "@babel/plugin-proposal-private-property-in-object" "^7.14.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.12.13" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.12.13" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.0" + "@babel/plugin-syntax-top-level-await" "^7.12.13" + "@babel/plugin-transform-arrow-functions" "^7.13.0" + "@babel/plugin-transform-async-to-generator" "^7.13.0" + "@babel/plugin-transform-block-scoped-functions" "^7.12.13" + "@babel/plugin-transform-block-scoping" "^7.14.2" + "@babel/plugin-transform-classes" "^7.14.2" + "@babel/plugin-transform-computed-properties" "^7.13.0" + "@babel/plugin-transform-destructuring" "^7.13.17" + "@babel/plugin-transform-dotall-regex" "^7.12.13" + "@babel/plugin-transform-duplicate-keys" "^7.12.13" + "@babel/plugin-transform-exponentiation-operator" "^7.12.13" + "@babel/plugin-transform-for-of" "^7.13.0" + "@babel/plugin-transform-function-name" "^7.12.13" + "@babel/plugin-transform-literals" "^7.12.13" + "@babel/plugin-transform-member-expression-literals" "^7.12.13" + "@babel/plugin-transform-modules-amd" "^7.14.2" + "@babel/plugin-transform-modules-commonjs" "^7.14.0" + "@babel/plugin-transform-modules-systemjs" "^7.13.8" + "@babel/plugin-transform-modules-umd" "^7.14.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.13" + "@babel/plugin-transform-new-target" "^7.12.13" + "@babel/plugin-transform-object-super" "^7.12.13" + "@babel/plugin-transform-parameters" "^7.14.2" + "@babel/plugin-transform-property-literals" "^7.12.13" + "@babel/plugin-transform-regenerator" "^7.13.15" + "@babel/plugin-transform-reserved-words" "^7.12.13" + "@babel/plugin-transform-shorthand-properties" "^7.12.13" + "@babel/plugin-transform-spread" "^7.13.0" + "@babel/plugin-transform-sticky-regex" "^7.12.13" + "@babel/plugin-transform-template-literals" "^7.13.0" + "@babel/plugin-transform-typeof-symbol" "^7.12.13" + "@babel/plugin-transform-unicode-escapes" "^7.12.13" + "@babel/plugin-transform-unicode-regex" "^7.12.13" + "@babel/preset-modules" "^0.1.4" + "@babel/types" "^7.14.2" + "babel-plugin-polyfill-corejs2" "^0.2.0" + "babel-plugin-polyfill-corejs3" "^0.2.0" + "babel-plugin-polyfill-regenerator" "^0.2.0" + "core-js-compat" "^3.9.0" + "semver" "^6.3.0" + +"@babel/preset-modules@^0.1.4": + "integrity" "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==" + "resolved" "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz" + "version" "0.1.4" + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + "esutils" "^2.0.2" + +"@babel/runtime@^7.8.4", "@babel/runtime@7.14.0": + "integrity" "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==" + "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz" + "version" "7.14.0" + dependencies: + "regenerator-runtime" "^0.13.4" + +"@babel/template@^7.12.13", "@babel/template@^7.8.3", "@babel/template@7.12.13": + "integrity" "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==" + "resolved" "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz" + "version" "7.12.13" + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + +"@babel/template@^7.14.5": + "integrity" "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==" + "resolved" "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.14.2", "@babel/traverse@^7.14.5", "@babel/traverse@^7.8.3": + "integrity" "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==" + "resolved" "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz" + "version" "7.14.7" + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.14.5" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/parser" "^7.14.7" + "@babel/types" "^7.14.5" + "debug" "^4.1.0" + "globals" "^11.1.0" + +"@babel/types@^7.12.13", "@babel/types@^7.14.2", "@babel/types@^7.14.5", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6": + "integrity" "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==" + "resolved" "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz" + "version" "7.14.5" + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + "to-fast-properties" "^2.0.0" + +"@cds/city@^1.1.0": + "integrity" "sha512-S9K+Q39BGOghyLHmR0Wdcmu1i1noSUk8HcvMj+3IaohZw02WFd99aPTQDHJeseXrXZP3CNovaSlePI0R11NcFg==" + "resolved" "https://registry.npmjs.org/@cds/city/-/city-1.1.0.tgz" + "version" "1.1.0" + +"@cds/core@^5.0.0", "@cds/core@^5.4.1": + "integrity" "sha512-7RhtrMx5UM6xCU+2l9T7E/MBZ+nDqGGyzOLz9EhscKjurIJQrGTPAIwgomJNZ22W2Gw+S36+v/R62y4uStQFTg==" + "resolved" "https://registry.npmjs.org/@cds/core/-/core-5.4.1.tgz" + "version" "5.4.1" + dependencies: + "lit" "2.0.0-rc.2" + "ramda" "^0.27.1" + "tslib" "^2.2.0" + optionalDependencies: + "@cds/city" "^1.1.0" + "@types/resize-observer-browser" "^0.1.5" + "normalize.css" "^8.0.1" + +"@clr/angular@^5.4.1": + "integrity" "sha512-3hsJE4+aVHSNh/jhc+UZPScD97SeOOEgTKL270wazojTdUl7MacJyclgYxekzTS+DWWfiT1Fd3v7ccKLD17sQA==" + "resolved" "https://registry.npmjs.org/@clr/angular/-/angular-5.4.1.tgz" + "version" "5.4.1" + dependencies: + "tslib" "^2.0.0" + +"@clr/icons@^5.4.1": + "integrity" "sha512-KDJJdCnnnbTK52zXpWY5sYsTWUFqX0iqZOE0Dpxk+p0oOeIGrzScreFOy8L1izko3QHw8ytfpG5+50hNxaokfQ==" + "resolved" "https://registry.npmjs.org/@clr/icons/-/icons-5.4.1.tgz" + "version" "5.4.1" + +"@clr/ui@^5.4.1", "@clr/ui@5.4.1": + "integrity" "sha512-E03VDj43moarX2mp4U48h9Nrx9N6foDPjzqTFqKvjFaAIzEs/5GM8cNlo6EizVBV7y3+MwoM72bT5XJTygRL6w==" + "resolved" "https://registry.npmjs.org/@clr/ui/-/ui-5.4.1.tgz" + "version" "5.4.1" + +"@csstools/convert-colors@^1.4.0": + "integrity" "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" + "resolved" "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz" + "version" "1.4.0" + +"@discoveryjs/json-ext@0.5.2": + "integrity" "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==" + "resolved" "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz" + "version" "0.5.2" + +"@gar/promisify@^1.0.1": + "integrity" "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==" + "resolved" "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz" + "version" "1.1.2" + +"@istanbuljs/schema@^0.1.2": + "integrity" "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" + "resolved" "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + "version" "0.1.3" + +"@jsdevtools/coverage-istanbul-loader@3.0.5": + "integrity" "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==" + "resolved" "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz" + "version" "3.0.5" + dependencies: + "convert-source-map" "^1.7.0" + "istanbul-lib-instrument" "^4.0.3" + "loader-utils" "^2.0.0" + "merge-source-map" "^1.1.0" + "schema-utils" "^2.7.0" + +"@lit/reactive-element@^1.0.0-rc.2": + "integrity" "sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==" + "resolved" "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz" + "version" "1.0.0-rc.2" + +"@ng-bootstrap/ng-bootstrap@^10.0.0": + "integrity" "sha512-Sz+QaxjuyJYJ+zyUbf0TevgcgVesCPQiiFiggEzxKjzY5R+Hvq3YgryLdXf2r/ryePL+C3FXCcmmKpTM5bfczQ==" + "resolved" "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-10.0.0.tgz" + "version" "10.0.0" + dependencies: + "tslib" "^2.1.0" + +"@ngtools/webpack@12.0.5": + "integrity" "sha512-yoKK6qhEm1iWnniz50xzOBqa3U1iUrjZs+SpLOXRZFonpwObz8j4zraR231K4uV4kXcX40qorYk9iOf+ljG4JQ==" + "resolved" "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "enhanced-resolve" "5.7.0" + +"@ngx-translate/core@^13.0.0", "@ngx-translate/core@>=13.0.0": + "integrity" "sha512-+tzEp8wlqEnw0Gc7jtVRAJ6RteUjXw6JJR4O65KlnxOmJrCGPI0xjV/lKRnQeU0w4i96PQs/jtpL921Wrb7PWg==" + "resolved" "https://registry.npmjs.org/@ngx-translate/core/-/core-13.0.0.tgz" + "version" "13.0.0" + dependencies: + "tslib" "^2.0.0" + +"@ngx-translate/http-loader@^6.0.0": + "integrity" "sha512-LCekn6qCbeXWlhESCxU1rAbZz33WzDG0lI7Ig0pYC1o5YxJWrkU9y3Y4tNi+jakQ7R6YhTR2D3ox6APxDtA0wA==" + "resolved" "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "tslib" "^2.0.0" + +"@nodelib/fs.scandir@2.1.5": + "integrity" "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + "version" "2.1.5" + dependencies: + "@nodelib/fs.stat" "2.0.5" + "run-parallel" "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + "integrity" "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + "version" "2.0.5" + +"@nodelib/fs.walk@^1.2.3": + "integrity" "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==" + "resolved" "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz" + "version" "1.2.7" + dependencies: + "@nodelib/fs.scandir" "2.1.5" + "fastq" "^1.6.0" + +"@npmcli/fs@^1.0.0": + "integrity" "sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA==" + "resolved" "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "@gar/promisify" "^1.0.1" + "semver" "^7.3.5" + +"@npmcli/git@^2.0.1", "@npmcli/git@^2.1.0": + "integrity" "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==" + "resolved" "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + "lru-cache" "^6.0.0" + "mkdirp" "^1.0.4" + "npm-pick-manifest" "^6.1.1" + "promise-inflight" "^1.0.1" + "promise-retry" "^2.0.1" + "semver" "^7.3.5" + "which" "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + "integrity" "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==" + "resolved" "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz" + "version" "1.0.7" + dependencies: + "npm-bundled" "^1.1.1" + "npm-normalize-package-bin" "^1.0.1" + +"@npmcli/move-file@^1.0.1": + "integrity" "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==" + "resolved" "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" + "version" "1.1.2" + dependencies: + "mkdirp" "^1.0.4" + "rimraf" "^3.0.2" + +"@npmcli/node-gyp@^1.0.2": + "integrity" "sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg==" + "resolved" "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz" + "version" "1.0.2" + +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + "integrity" "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==" + "resolved" "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "infer-owner" "^1.0.4" + +"@npmcli/run-script@^1.8.2": + "integrity" "sha512-NQspusBCpTjNwNRFMtz2C5MxoxyzlbuJ4YEhxAKrIonTiirKDtatsZictx9RgamQIx6+QuHMNmPl0wQdoESs9A==" + "resolved" "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.5.tgz" + "version" "1.8.5" + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + "infer-owner" "^1.0.4" + "node-gyp" "^7.1.0" + "read-package-json-fast" "^2.0.1" + +"@npmcli/run-script@^2.0.0": + "integrity" "sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig==" + "resolved" "https://registry.npmjs.org/@npmcli/run-script/-/run-script-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + "node-gyp" "^8.2.0" + "read-package-json-fast" "^2.0.1" + +"@schematics/angular@12.0.5": + "integrity" "sha512-gMT66T33az+uGLDSc7UkJVg+vloPeTpQNgWddBVGnW/Lkl1tGaWUxyqUJAp8AvusPNU+NCP+ZFB3qUm+pc7tCg==" + "resolved" "https://registry.npmjs.org/@schematics/angular/-/angular-12.0.5.tgz" + "version" "12.0.5" + dependencies: + "@angular-devkit/core" "12.0.5" + "@angular-devkit/schematics" "12.0.5" + "jsonc-parser" "3.0.0" + +"@sindresorhus/is@^0.14.0": + "integrity" "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + "resolved" "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" + "version" "0.14.0" + +"@szmarczak/http-timer@^1.1.2": + "integrity" "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==" + "resolved" "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" + "version" "1.1.2" + dependencies: + "defer-to-connect" "^1.0.1" + +"@tootallnate/once@1": + "integrity" "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + "resolved" "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" + "version" "1.1.2" + +"@trysound/sax@0.1.1": + "integrity" "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==" + "resolved" "https://registry.npmjs.org/@trysound/sax/-/sax-0.1.1.tgz" + "version" "0.1.1" + +"@types/component-emitter@^1.2.10": + "integrity" "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + "resolved" "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz" + "version" "1.2.10" + +"@types/cookie@^0.4.0": + "integrity" "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + "resolved" "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz" + "version" "0.4.0" + +"@types/cors@^2.8.8": + "integrity" "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" + "resolved" "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz" + "version" "2.8.10" + +"@types/d3-array@*": + "integrity" "sha512-D/G7oG0czeszALrkdUiV68CDiHDxXf+M2mLVqAyKktGd12VKQQljj1sHJGBKjcK4jRH1biBd6ZPQPHpJ0mNa0w==" + "resolved" "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-array@^1": + "integrity" "sha512-E/7RgPr2ylT5dWG0CswMi9NpFcjIEDqLcUSBgNHe/EMahfqYaTx4zhcggG3khqoEB/leY4Vl6nTSbwLUPjXceA==" + "resolved" "https://registry.npmjs.org/@types/d3-array/-/d3-array-1.2.9.tgz" + "version" "1.2.9" + +"@types/d3-axis@*": + "integrity" "sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==" + "resolved" "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-selection" "*" + +"@types/d3-axis@^1": + "integrity" "sha512-p7085weOmo4W+DzlRRVC/7OI/jugaKbVa6WMQGCQscaMylcbuaVEGk7abJLNyGVFLeCBNrHTdDiqRGnzvL0nXQ==" + "resolved" "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.16.tgz" + "version" "1.0.16" + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-brush@*": + "integrity" "sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==" + "resolved" "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@^1": + "integrity" "sha512-4zGkBafJf5zCsBtLtvDj/pNMo5X9+Ii/1hUz0GvQ+wEwelUBm2AbIDAzJnp2hLDFF307o0fhxmmocHclhXC+tw==" + "resolved" "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-1.1.5.tgz" + "version" "1.1.5" + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-chord@*": + "integrity" "sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==" + "resolved" "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-chord@^1": + "integrity" "sha512-0DdfJ//bxyW3G9Nefwq/LDgazSKNN8NU0lBT3Cza6uVuInC2awMNsAcv1oKyRFLn9z7kXClH5XjwpveZjuz2eg==" + "resolved" "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.11.tgz" + "version" "1.0.11" + +"@types/d3-collection@*": + "integrity" "sha512-54Fdv8u5JbuXymtmXm2SYzi1x/Svt+jfWBU5junkhrCewL92VjqtCBDn97coBRVwVFmYNnVTNDyV8gQyPYfm+A==" + "resolved" "https://registry.npmjs.org/@types/d3-collection/-/d3-collection-1.0.10.tgz" + "version" "1.0.10" + +"@types/d3-color@*": + "integrity" "sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ==" + "resolved" "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz" + "version" "3.0.2" + +"@types/d3-color@^1": + "integrity" "sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA==" + "resolved" "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.2.tgz" + "version" "1.4.2" + +"@types/d3-contour@*": + "integrity" "sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==" + "resolved" "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-contour@^1": + "integrity" "sha512-LxwmGIfVJIc1cKs7ZFRQ1FbtXpfH7QTXYRdMIJsFP71uCMdF6jJ0XZakYDX6Hn4yZkLf+7V8FgD34yCcok+5Ww==" + "resolved" "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-1.3.3.tgz" + "version" "1.3.3" + dependencies: + "@types/d3-array" "^1" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + "integrity" "sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w==" + "resolved" "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz" + "version" "6.0.0" + +"@types/d3-dispatch@*": + "integrity" "sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==" + "resolved" "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-dispatch@^1": + "integrity" "sha512-zJ44YgjqALmyps+II7b1mZLhrtfV/FOxw9owT87mrweGWcg+WK5oiJX2M3SYJ0XUAExBduarysfgbR11YxzojQ==" + "resolved" "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-1.0.9.tgz" + "version" "1.0.9" + +"@types/d3-drag@*": + "integrity" "sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==" + "resolved" "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-selection" "*" + +"@types/d3-drag@^1": + "integrity" "sha512-7NeTnfolst1Js3Vs7myctBkmJWu6DMI3k597AaHUX98saHjHWJ6vouT83UrpE+xfbSceHV+8A0JgxuwgqgmqWw==" + "resolved" "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.5.tgz" + "version" "1.2.5" + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-dsv@*": + "integrity" "sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==" + "resolved" "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-dsv@^1": + "integrity" "sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA==" + "resolved" "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.2.1.tgz" + "version" "1.2.1" + +"@types/d3-ease@*": + "integrity" "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + "resolved" "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-ease@^1": + "integrity" "sha512-fMFTCzd8DOwruE9zlu2O8ci5ct+U5jkGcDS+cH+HCidnJlDs0MZ+TuSVCFtEzh4E5MasItwy+HvgoFtxPHa5Cw==" + "resolved" "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-1.0.10.tgz" + "version" "1.0.10" + +"@types/d3-fetch@*": + "integrity" "sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==" + "resolved" "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-fetch@^1": + "integrity" "sha512-rtFs92GugtV/NpiJQd0WsmGLcg52tIL0uF0bKbbJg231pR9JEb6HT4AUwrtuLq3lOeKdLBhsjV14qb0pMmd0Aw==" + "resolved" "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-1.2.2.tgz" + "version" "1.2.2" + dependencies: + "@types/d3-dsv" "^1" + +"@types/d3-force@*": + "integrity" "sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==" + "resolved" "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz" + "version" "3.0.3" + +"@types/d3-force@^1": + "integrity" "sha512-fkorLTKvt6AQbFBQwn4aq7h9rJ4c7ZVcPMGB8X6eFFveAyMZcv7t7m6wgF4Eg93rkPgPORU7sAho1QSHNcZu6w==" + "resolved" "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.2.4.tgz" + "version" "1.2.4" + +"@types/d3-format@*": + "integrity" "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + "resolved" "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-format@^1": + "integrity" "sha512-WeGCHAs7PHdZYq6lwl/+jsl+Nfc1J2W1kNcMeIMYzQsT6mtBDBgtJ/rcdjZ0k0rVIvqEZqhhuD5TK/v3P2gFHQ==" + "resolved" "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.2.tgz" + "version" "1.4.2" + +"@types/d3-geo@*": + "integrity" "sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==" + "resolved" "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "@types/geojson" "*" + +"@types/d3-geo@^1": + "integrity" "sha512-yZbPb7/5DyL/pXkeOmZ7L5ySpuGr4H48t1cuALjnJy5sXQqmSSAYBiwa6Ya/XpWKX2rJqGDDubmh3nOaopOpeA==" + "resolved" "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.3.tgz" + "version" "1.12.3" + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + "integrity" "sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ==" + "resolved" "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz" + "version" "3.0.2" + +"@types/d3-hierarchy@^1": + "integrity" "sha512-AbStKxNyWiMDQPGDguG2Kuhlq1Sv539pZSxYbx4UZeYkutpPwXCcgyiRrlV4YH64nIOsKx7XVnOMy9O7rJsXkg==" + "resolved" "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz" + "version" "1.1.8" + +"@types/d3-interpolate@*": + "integrity" "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==" + "resolved" "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-color" "*" + +"@types/d3-interpolate@^1": + "integrity" "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==" + "resolved" "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz" + "version" "1.4.2" + dependencies: + "@types/d3-color" "^1" + +"@types/d3-path@*": + "integrity" "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + "resolved" "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-path@^1": + "integrity" "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==" + "resolved" "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz" + "version" "1.0.9" + +"@types/d3-polygon@*": + "integrity" "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==" + "resolved" "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-polygon@^1": + "integrity" "sha512-1TOJPXCBJC9V3+K3tGbTqD/CsqLyv/YkTXAcwdsZzxqw5cvpdnCuDl42M4Dvi8XzMxZNCT9pL4ibrK2n4VmAcw==" + "resolved" "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-1.0.8.tgz" + "version" "1.0.8" + +"@types/d3-quadtree@*": + "integrity" "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==" + "resolved" "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz" + "version" "3.0.2" + +"@types/d3-quadtree@^1": + "integrity" "sha512-5E0OJJn2QVavITFEc1AQlI8gLcIoDZcTKOD3feKFckQVmFV4CXhqRFt83tYNVNIN4ZzRkjlAMavJa1ldMhf5rA==" + "resolved" "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-1.0.9.tgz" + "version" "1.0.9" + +"@types/d3-random@*": + "integrity" "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==" + "resolved" "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-random@^1": + "integrity" "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ==" + "resolved" "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.3.tgz" + "version" "1.1.3" + +"@types/d3-scale-chromatic@*": + "integrity" "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + "resolved" "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-scale-chromatic@^1": + "integrity" "sha512-7FtJYrmXTEWLykShjYhoGuDNR/Bda0+tstZMkFj4RRxUEryv16AGh3be21tqg84B6KfEwiZyEpBcTyPyU+GWjg==" + "resolved" "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.5.1.tgz" + "version" "1.5.1" + +"@types/d3-scale@*": + "integrity" "sha512-GDuXcRcR6mKcpUVMhPNttpOzHi2dP6YcDqLZYSZHgwTZ+sfCa8e9q0VEBwZomblAPNMYpVqxojnSyIEb4s/Pwg==" + "resolved" "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "@types/d3-time" "*" + +"@types/d3-scale@^2": + "integrity" "sha512-CHu34T5bGrJOeuhGxyiz9Xvaa9PlsIaQoOqjDg7zqeGj2x0rwPhGquiy03unigvcMxmvY0hEaAouT0LOFTLpIw==" + "resolved" "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.6.tgz" + "version" "2.2.6" + dependencies: + "@types/d3-time" "^1" + +"@types/d3-selection@*": + "integrity" "sha512-aJ1d1SCUtERHH65bB8NNoLpUOI3z8kVcfg2BGm4rMMUwuZF4x6qnIEKjT60Vt0o7gP/a/xkRVs4D9CpDifbyRA==" + "resolved" "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.1.tgz" + "version" "3.0.1" + +"@types/d3-selection@^1": + "integrity" "sha512-GjKQWVZO6Sa96HiKO6R93VBE8DUW+DDkFpIMf9vpY5S78qZTlRRSNUsHr/afDpF7TvLDV7VxrUFOWW7vdIlYkA==" + "resolved" "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.3.tgz" + "version" "1.4.3" + +"@types/d3-shape@*": + "integrity" "sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw==" + "resolved" "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "@types/d3-path" "*" + +"@types/d3-shape@^1": + "integrity" "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==" + "resolved" "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz" + "version" "1.3.8" + dependencies: + "@types/d3-path" "^1" + +"@types/d3-time-format@*": + "integrity" "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==" + "resolved" "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz" + "version" "4.0.0" + +"@types/d3-time-format@^2": + "integrity" "sha512-fck0Z9RGfIQn3GJIEKVrp15h9m6Vlg0d5XXeiE/6+CQiBmMDZxfR21XtjEPuDeg7gC3bBM0SdieA5XF3GW1wKA==" + "resolved" "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.1.tgz" + "version" "2.3.1" + +"@types/d3-time@*": + "integrity" "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + "resolved" "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-time@^1": + "integrity" "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw==" + "resolved" "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz" + "version" "1.1.1" + +"@types/d3-timer@*": + "integrity" "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + "resolved" "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz" + "version" "3.0.0" + +"@types/d3-timer@^1": + "integrity" "sha512-ZnAbquVqy+4ZjdW0cY6URp+qF/AzTVNda2jYyOzpR2cPT35FTXl78s15Bomph9+ckOiI1TtkljnWkwbIGAb6rg==" + "resolved" "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-1.0.10.tgz" + "version" "1.0.10" + +"@types/d3-transition@*": + "integrity" "sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==" + "resolved" "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-selection" "*" + +"@types/d3-transition@^1": + "integrity" "sha512-J+a3SuF/E7wXbOSN19p8ZieQSFIm5hU2Egqtndbc54LXaAEOpLfDx4sBu/PKAKzHOdgKK1wkMhINKqNh4aoZAg==" + "resolved" "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "@types/d3-selection" "^1" + +"@types/d3-voronoi@*": + "integrity" "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==" + "resolved" "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz" + "version" "1.1.9" + +"@types/d3-zoom@*": + "integrity" "sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==" + "resolved" "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3-zoom@^1": + "integrity" "sha512-3kHkL6sPiDdbfGhzlp5gIHyu3kULhtnHTTAl3UBZVtWB1PzcLL8vdmz5mTx7plLiUqOA2Y+yT2GKjt/TdA2p7Q==" + "resolved" "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.8.3.tgz" + "version" "1.8.3" + dependencies: + "@types/d3-interpolate" "^1" + "@types/d3-selection" "^1" + +"@types/d3@^5": + "integrity" "sha512-2u0O9iP1MubFiQ+AhR1id4Egs+07BLtvRATG6IL2Gs9+KzdrfaxCKNq5hxEyw1kxwsqB/lCgr108XuHcKtb/5w==" + "resolved" "https://registry.npmjs.org/@types/d3/-/d3-5.16.4.tgz" + "version" "5.16.4" + dependencies: + "@types/d3-array" "^1" + "@types/d3-axis" "^1" + "@types/d3-brush" "^1" + "@types/d3-chord" "^1" + "@types/d3-collection" "*" + "@types/d3-color" "^1" + "@types/d3-contour" "^1" + "@types/d3-dispatch" "^1" + "@types/d3-drag" "^1" + "@types/d3-dsv" "^1" + "@types/d3-ease" "^1" + "@types/d3-fetch" "^1" + "@types/d3-force" "^1" + "@types/d3-format" "^1" + "@types/d3-geo" "^1" + "@types/d3-hierarchy" "^1" + "@types/d3-interpolate" "^1" + "@types/d3-path" "^1" + "@types/d3-polygon" "^1" + "@types/d3-quadtree" "^1" + "@types/d3-random" "^1" + "@types/d3-scale" "^2" + "@types/d3-scale-chromatic" "^1" + "@types/d3-selection" "^1" + "@types/d3-shape" "^1" + "@types/d3-time" "^1" + "@types/d3-time-format" "^2" + "@types/d3-timer" "^1" + "@types/d3-transition" "^1" + "@types/d3-voronoi" "*" + "@types/d3-zoom" "^1" + +"@types/d3@^7.0.0": + "integrity" "sha512-7rMMuS5unvbvFCJXAkQXIxWTo2OUlmVXN5q7sfQFesuVICY55PSP6hhbUhWjTTNpfTTB3iLALsIYDFe7KUNABw==" + "resolved" "https://registry.npmjs.org/@types/d3/-/d3-7.0.0.tgz" + "version" "7.0.0" + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + +"@types/dagre-d3@^0.6.3": + "integrity" "sha512-qPd9DtaIbZArKqvEXXHrY1uljoFVQs812aIMCzJPX5TulVz/jEyqD05SJT9Oaan8abszUdqUjlXom9aZvLWpew==" + "resolved" "https://registry.npmjs.org/@types/dagre-d3/-/dagre-d3-0.6.3.tgz" + "version" "0.6.3" + dependencies: + "@types/d3" "^5" + "@types/dagre" "*" + "@types/graphlib" "*" + +"@types/dagre@*": + "integrity" "sha512-ku3y+F8sPqmiB5Ugl22NpukI2dLAViJiWwdtueXLeuF4fxZozl+bytPSFVlLu/gDgaKiwobu3LBXXRWktIMiIA==" + "resolved" "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.46.tgz" + "version" "0.7.46" + +"@types/eslint-scope@^3.7.0": + "integrity" "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==" + "resolved" "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz" + "version" "3.7.0" + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + "integrity" "sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg==" + "resolved" "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.13.tgz" + "version" "7.2.13" + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.47": + "integrity" "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==" + "resolved" "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz" + "version" "0.0.47" + +"@types/file-saver@^2.0.3": + "integrity" "sha512-MBIou8pd/41jkff7s97B47bc9+p0BszqqDJsO51yDm49uUxeKzrfuNl5fSLC6BpLEWKA8zlwyqALVmXrFwoBHQ==" + "resolved" "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.3.tgz" + "version" "2.0.3" + +"@types/geojson@*": + "integrity" "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" + "resolved" "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz" + "version" "7946.0.8" + +"@types/glob@^7.1.1": + "integrity" "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==" + "resolved" "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz" + "version" "7.1.3" + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/graphlib@*": + "integrity" "sha512-8nbbyD3zABRA9ePoBgAl2ym8cIwKQXTfv1gaIRTdY99yEOCaHfmjBeRp+BIemS8NtOqoWK7mfzWxjNrxLK3T5w==" + "resolved" "https://registry.npmjs.org/@types/graphlib/-/graphlib-2.1.8.tgz" + "version" "2.1.8" + +"@types/jasmine@~3.6.0": + "integrity" "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==" + "resolved" "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz" + "version" "3.6.11" + +"@types/jquery@^3.5.9": + "integrity" "sha512-B8pDk+sH/tSv/HKdx6EQER6BfUOb2GtKs0LOmozziS4h7cbe8u/eYySfUAeTwD+J09SqV3man7AMWIA5mgzCBA==" + "resolved" "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.9.tgz" + "version" "3.5.9" + dependencies: + "@types/sizzle" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": + "integrity" "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + "resolved" "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz" + "version" "7.0.7" + +"@types/minimatch@*": + "integrity" "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" + "resolved" "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz" + "version" "3.0.4" + +"@types/mockjs@^1.0.4": + "integrity" "sha512-gK20xPqJhzMIitechVbvfnAk+oBIxVRnWrihJpRYHMI6UHCB/cvWgJa+dy6trRwQLE3AbtAJnXpm7pn6blG8sA==" + "resolved" "https://registry.npmjs.org/@types/mockjs/-/mockjs-1.0.4.tgz" + "version" "1.0.4" + +"@types/node@*", "@types/node@^12.11.1", "@types/node@>=10.0.0": + "integrity" "sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==" + "resolved" "https://registry.npmjs.org/@types/node/-/node-12.20.15.tgz" + "version" "12.20.15" + +"@types/parse-json@^4.0.0": + "integrity" "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "resolved" "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + "version" "4.0.0" + +"@types/resize-observer-browser@^0.1.5": + "integrity" "sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==" + "resolved" "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz" + "version" "0.1.5" + +"@types/sizzle@*": + "integrity" "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" + "resolved" "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz" + "version" "2.3.3" + +"@types/source-list-map@*": + "integrity" "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" + "resolved" "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz" + "version" "0.1.2" + +"@types/trusted-types@^1.0.1": + "integrity" "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==" + "resolved" "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz" + "version" "1.0.6" + +"@types/webpack-sources@^0.1.5": + "integrity" "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==" + "resolved" "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz" + "version" "0.1.8" + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + "source-map" "^0.6.1" + +"@webassemblyjs/ast@1.11.0": + "integrity" "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/helper-numbers" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + +"@webassemblyjs/floating-point-hex-parser@1.11.0": + "integrity" "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz" + "version" "1.11.0" + +"@webassemblyjs/helper-api-error@1.11.0": + "integrity" "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz" + "version" "1.11.0" + +"@webassemblyjs/helper-buffer@1.11.0": + "integrity" "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz" + "version" "1.11.0" + +"@webassemblyjs/helper-numbers@1.11.0": + "integrity" "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.0": + "integrity" "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz" + "version" "1.11.0" + +"@webassemblyjs/helper-wasm-section@1.11.0": + "integrity" "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + +"@webassemblyjs/ieee754@1.11.0": + "integrity" "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.0": + "integrity" "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.0": + "integrity" "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz" + "version" "1.11.0" + +"@webassemblyjs/wasm-edit@1.11.0": + "integrity" "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/helper-wasm-section" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-opt" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + "@webassemblyjs/wast-printer" "1.11.0" + +"@webassemblyjs/wasm-gen@1.11.0": + "integrity" "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" + +"@webassemblyjs/wasm-opt@1.11.0": + "integrity" "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + +"@webassemblyjs/wasm-parser@1.11.0": + "integrity" "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" + +"@webassemblyjs/wast-printer@1.11.0": + "integrity" "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==" + "resolved" "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@xtuc/long" "4.2.2" + +"@webcomponents/webcomponentsjs@^2.5.0": + "integrity" "sha512-C0l51MWQZ9kLzcxOZtniOMohpIFdCLZum7/TEHv3XWFc1Fvt5HCpbSX84x8ltka/JuNKcuiDnxXFkiB2gaePcg==" + "resolved" "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.5.0.tgz" + "version" "2.5.0" + +"@xtuc/ieee754@^1.2.0": + "integrity" "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "resolved" "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + "version" "1.2.0" + +"@xtuc/long@4.2.2": + "integrity" "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "resolved" "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + "version" "4.2.2" + +"@yarnpkg/lockfile@1.1.0": + "integrity" "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + "resolved" "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" + "version" "1.1.0" + +"abab@^2.0.5": + "integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + "resolved" "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz" + "version" "2.0.5" + +"abbrev@1": + "integrity" "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "resolved" "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + "version" "1.1.1" + +"accepts@~1.3.4", "accepts@~1.3.5", "accepts@~1.3.7": + "integrity" "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==" + "resolved" "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" + "version" "1.3.7" + dependencies: + "mime-types" "~2.1.24" + "negotiator" "0.6.2" + +"acorn@^8.2.1": + "integrity" "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" + "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz" + "version" "8.4.1" + +"adjust-sourcemap-loader@^4.0.0": + "integrity" "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==" + "resolved" "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "loader-utils" "^2.0.0" + "regex-parser" "^2.2.11" + +"agent-base@^6.0.2", "agent-base@6": + "integrity" "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==" + "resolved" "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + "version" "6.0.2" + dependencies: + "debug" "4" + +"agentkeepalive@^4.1.3": + "integrity" "sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==" + "resolved" "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz" + "version" "4.1.4" + dependencies: + "debug" "^4.1.0" + "depd" "^1.1.2" + "humanize-ms" "^1.2.1" + +"aggregate-error@^3.0.0": + "integrity" "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==" + "resolved" "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "clean-stack" "^2.0.0" + "indent-string" "^4.0.0" + +"ajv-errors@^1.0.0": + "integrity" "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + "resolved" "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz" + "version" "1.0.1" + +"ajv-formats@2.0.2": + "integrity" "sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw==" + "resolved" "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "ajv" "^8.0.0" + +"ajv-keywords@^3.1.0", "ajv-keywords@^3.5.2": + "integrity" "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "resolved" "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + "version" "3.5.2" + +"ajv@^6.1.0": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + 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" + +"ajv@^6.12.3": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + 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" + +"ajv@^6.12.4": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + 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" + +"ajv@^6.12.5": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + 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" + +"ajv@^6.9.1", "ajv@^8.0.0", "ajv@>=5.0.0", "ajv@8.2.0": + "integrity" "sha512-WSNGFuyWd//XO8n/m/EaOlNLtO0yL8EXT/74LqT4khdhpZjP7lkj/kT5uwRmGitKEVp/Oj7ZUHeGfPtgHhQ5CA==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-8.2.0.tgz" + "version" "8.2.0" + dependencies: + "fast-deep-equal" "^3.1.1" + "json-schema-traverse" "^1.0.0" + "require-from-string" "^2.0.2" + "uri-js" "^4.2.2" + +"alphanum-sort@^1.0.2": + "integrity" "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + "resolved" "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz" + "version" "1.0.2" + +"angular-material@^1.2.2": + "integrity" "sha512-GtrBzYDg5zHYX/OUqxo72zb9bsY0RDuIA+4ZsZjuv1r+L4q9UAtkeIZzMXVGua6CCNgfB58cKep3TmxtAZhNwg==" + "resolved" "https://registry.npmjs.org/angular-material/-/angular-material-1.2.2.tgz" + "version" "1.2.2" + +"ansi-align@^3.0.0": + "integrity" "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==" + "resolved" "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "string-width" "^4.1.0" + +"ansi-colors@^3.0.0": + "integrity" "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==" + "resolved" "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz" + "version" "3.2.4" + +"ansi-colors@4.1.1": + "integrity" "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + "resolved" "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" + "version" "4.1.1" + +"ansi-escapes@^4.2.1": + "integrity" "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==" + "resolved" "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + "version" "4.3.2" + dependencies: + "type-fest" "^0.21.3" + +"ansi-html@0.0.7": + "integrity" "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + "resolved" "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz" + "version" "0.0.7" + +"ansi-regex@^2.0.0": + "integrity" "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + "version" "2.1.1" + +"ansi-regex@^3.0.0": + "integrity" "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz" + "version" "3.0.0" + +"ansi-regex@^4.1.0": + "integrity" "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz" + "version" "4.1.0" + +"ansi-regex@^5.0.1": + "integrity" "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + "version" "5.0.1" + +"ansi-styles@^3.2.0", "ansi-styles@^3.2.1": + "integrity" "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + "version" "3.2.1" + dependencies: + "color-convert" "^1.9.0" + +"ansi-styles@^4.0.0", "ansi-styles@^4.1.0": + "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "color-convert" "^2.0.1" + +"ansi-styles@^4.1.0": + "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "color-convert" "^2.0.1" + +"anymatch@^2.0.0": + "integrity" "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==" + "resolved" "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "micromatch" "^3.1.4" + "normalize-path" "^2.1.1" + +"anymatch@~3.1.2": + "integrity" "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==" + "resolved" "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" + "version" "3.1.2" + dependencies: + "normalize-path" "^3.0.0" + "picomatch" "^2.0.4" + +"aproba@^1.0.3", "aproba@^1.0.3 || ^2.0.0": + "integrity" "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "resolved" "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" + "version" "1.2.0" + +"are-we-there-yet@^2.0.0": + "integrity" "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==" + "resolved" "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "delegates" "^1.0.0" + "readable-stream" "^3.6.0" + +"are-we-there-yet@~1.1.2": + "integrity" "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==" + "resolved" "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz" + "version" "1.1.5" + dependencies: + "delegates" "^1.0.0" + "readable-stream" "^2.0.6" + +"argparse@^2.0.1": + "integrity" "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "resolved" "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + "version" "2.0.1" + +"arr-diff@^4.0.0": + "integrity" "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "resolved" "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" + "version" "4.0.0" + +"arr-flatten@^1.1.0": + "integrity" "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "resolved" "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz" + "version" "1.1.0" + +"arr-union@^3.1.0": + "integrity" "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "resolved" "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz" + "version" "3.1.0" + +"array-flatten@^2.1.0": + "integrity" "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + "resolved" "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" + "version" "2.1.2" + +"array-flatten@1.1.1": + "integrity" "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "resolved" "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + "version" "1.1.1" + +"array-union@^1.0.1": + "integrity" "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=" + "resolved" "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "array-uniq" "^1.0.1" + +"array-union@^2.1.0": + "integrity" "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "resolved" "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + "version" "2.1.0" + +"array-uniq@^1.0.1": + "integrity" "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + "resolved" "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" + "version" "1.0.3" + +"array-unique@^0.3.2": + "integrity" "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "resolved" "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" + "version" "0.3.2" + +"asn1@~0.2.3": + "integrity" "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==" + "resolved" "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz" + "version" "0.2.4" + dependencies: + "safer-buffer" "~2.1.0" + +"assert-plus@^1.0.0", "assert-plus@1.0.0": + "integrity" "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "resolved" "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + "version" "1.0.0" + +"assign-symbols@^1.0.0": + "integrity" "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "resolved" "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz" + "version" "1.0.0" + +"async-each@^1.0.1": + "integrity" "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + "resolved" "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz" + "version" "1.0.3" + +"async-limiter@~1.0.0": + "integrity" "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + "resolved" "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" + "version" "1.0.1" + +"async@^2.6.2": + "integrity" "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==" + "resolved" "https://registry.npmjs.org/async/-/async-2.6.3.tgz" + "version" "2.6.3" + dependencies: + "lodash" "^4.17.14" + +"asynckit@^0.4.0": + "integrity" "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "resolved" "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + "version" "0.4.0" + +"atob@^2.1.2": + "integrity" "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "resolved" "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" + "version" "2.1.2" + +"autoprefixer@^9.6.1": + "integrity" "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==" + "resolved" "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz" + "version" "9.8.6" + dependencies: + "browserslist" "^4.12.0" + "caniuse-lite" "^1.0.30001109" + "colorette" "^1.2.1" + "normalize-range" "^0.1.2" + "num2fraction" "^1.2.2" + "postcss" "^7.0.32" + "postcss-value-parser" "^4.1.0" + +"aws-sign2@~0.7.0": + "integrity" "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "resolved" "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + "version" "0.7.0" + +"aws4@^1.8.0": + "integrity" "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "resolved" "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" + "version" "1.11.0" + +"babel-loader@8.2.2": + "integrity" "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==" + "resolved" "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz" + "version" "8.2.2" + dependencies: + "find-cache-dir" "^3.3.1" + "loader-utils" "^1.4.0" + "make-dir" "^3.1.0" + "schema-utils" "^2.6.5" + +"babel-plugin-dynamic-import-node@^2.3.3": + "integrity" "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==" + "resolved" "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" + "version" "2.3.3" + dependencies: + "object.assign" "^4.1.0" + +"babel-plugin-polyfill-corejs2@^0.2.0": + "integrity" "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz" + "version" "0.2.2" + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.2.2" + "semver" "^6.1.1" + +"babel-plugin-polyfill-corejs3@^0.2.0": + "integrity" "sha512-rCOFzEIJpJEAU14XCcV/erIf/wZQMmMT5l5vXOpL5uoznyOGfDIjPj6FVytMvtzaKSTSVKouOCTPJ5OMUZH30g==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.3.tgz" + "version" "0.2.3" + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + "core-js-compat" "^3.14.0" + +"babel-plugin-polyfill-regenerator@^0.2.0": + "integrity" "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==" + "resolved" "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz" + "version" "0.2.2" + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + +"balanced-match@^1.0.0": + "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + "version" "1.0.2" + +"base@^0.11.1": + "integrity" "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==" + "resolved" "https://registry.npmjs.org/base/-/base-0.11.2.tgz" + "version" "0.11.2" + dependencies: + "cache-base" "^1.0.1" + "class-utils" "^0.3.5" + "component-emitter" "^1.2.1" + "define-property" "^1.0.0" + "isobject" "^3.0.1" + "mixin-deep" "^1.2.0" + "pascalcase" "^0.1.1" + +"base64-arraybuffer@0.1.4": + "integrity" "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + "resolved" "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz" + "version" "0.1.4" + +"base64-js@^1.3.1": + "integrity" "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "resolved" "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + "version" "1.5.1" + +"base64id@~2.0.0", "base64id@2.0.0": + "integrity" "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + "resolved" "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz" + "version" "2.0.0" + +"batch@0.6.1": + "integrity" "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + "resolved" "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + "version" "0.6.1" + +"bcrypt-pbkdf@^1.0.0": + "integrity" "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=" + "resolved" "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "tweetnacl" "^0.14.3" + +"big.js@^5.2.2": + "integrity" "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + "resolved" "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + "version" "5.2.2" + +"binary-extensions@^1.0.0": + "integrity" "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz" + "version" "1.13.1" + +"binary-extensions@^2.0.0": + "integrity" "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + "version" "2.2.0" + +"bindings@^1.5.0": + "integrity" "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==" + "resolved" "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + "version" "1.5.0" + dependencies: + "file-uri-to-path" "1.0.0" + +"bl@^4.1.0": + "integrity" "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==" + "resolved" "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "buffer" "^5.5.0" + "inherits" "^2.0.4" + "readable-stream" "^3.4.0" + +"body-parser@^1.19.0", "body-parser@1.19.0": + "integrity" "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==" + "resolved" "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz" + "version" "1.19.0" + 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" + +"bonjour@^3.5.0": + "integrity" "sha1-jokKGD2O6aI5OzhExpGkK897yfU=" + "resolved" "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz" + "version" "3.5.0" + dependencies: + "array-flatten" "^2.1.0" + "deep-equal" "^1.0.1" + "dns-equal" "^1.0.0" + "dns-txt" "^2.0.2" + "multicast-dns" "^6.0.1" + "multicast-dns-service-types" "^1.1.0" + +"boolbase@^1.0.0": + "integrity" "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + "resolved" "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + "version" "1.0.0" + +"bootstrap@^4.5.0": + "integrity" "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==" + "resolved" "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz" + "version" "4.6.0" + +"boxen@^5.0.0": + "integrity" "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==" + "resolved" "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "ansi-align" "^3.0.0" + "camelcase" "^6.2.0" + "chalk" "^4.1.0" + "cli-boxes" "^2.2.1" + "string-width" "^4.2.2" + "type-fest" "^0.20.2" + "widest-line" "^3.1.0" + "wrap-ansi" "^7.0.0" + +"brace-expansion@^1.1.7": + "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" + "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + "version" "1.1.11" + dependencies: + "balanced-match" "^1.0.0" + "concat-map" "0.0.1" + +"braces@^2.3.1", "braces@^2.3.2": + "integrity" "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==" + "resolved" "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz" + "version" "2.3.2" + dependencies: + "arr-flatten" "^1.1.0" + "array-unique" "^0.3.2" + "extend-shallow" "^2.0.1" + "fill-range" "^4.0.0" + "isobject" "^3.0.1" + "repeat-element" "^1.1.2" + "snapdragon" "^0.8.1" + "snapdragon-node" "^2.0.1" + "split-string" "^3.0.2" + "to-regex" "^3.0.1" + +"braces@^3.0.1", "braces@^3.0.2", "braces@~3.0.2": + "integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==" + "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "fill-range" "^7.0.1" + +"browserslist@^4.0.0", "browserslist@^4.12.0", "browserslist@^4.14.5", "browserslist@^4.16.0", "browserslist@^4.16.6", "browserslist@^4.6.4", "browserslist@^4.9.1": + "integrity" "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==" + "resolved" "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz" + "version" "4.16.6" + dependencies: + "caniuse-lite" "^1.0.30001219" + "colorette" "^1.2.2" + "electron-to-chromium" "^1.3.723" + "escalade" "^3.1.1" + "node-releases" "^1.1.71" + +"buffer-from@^1.0.0": + "integrity" "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "resolved" "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz" + "version" "1.1.1" + +"buffer-indexof@^1.0.0": + "integrity" "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + "resolved" "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz" + "version" "1.1.1" + +"buffer@^5.5.0": + "integrity" "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==" + "resolved" "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + "version" "5.7.1" + dependencies: + "base64-js" "^1.3.1" + "ieee754" "^1.1.13" + +"builtins@^1.0.3": + "integrity" "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + "resolved" "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz" + "version" "1.0.3" + +"bytes@3.0.0": + "integrity" "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "resolved" "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + "version" "3.0.0" + +"bytes@3.1.0": + "integrity" "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "resolved" "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz" + "version" "3.1.0" + +"cacache@^15.0.5", "cacache@15.0.6": + "integrity" "sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w==" + "resolved" "https://registry.npmjs.org/cacache/-/cacache-15.0.6.tgz" + "version" "15.0.6" + dependencies: + "@npmcli/move-file" "^1.0.1" + "chownr" "^2.0.0" + "fs-minipass" "^2.0.0" + "glob" "^7.1.4" + "infer-owner" "^1.0.4" + "lru-cache" "^6.0.0" + "minipass" "^3.1.1" + "minipass-collect" "^1.0.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.2" + "mkdirp" "^1.0.3" + "p-map" "^4.0.0" + "promise-inflight" "^1.0.1" + "rimraf" "^3.0.2" + "ssri" "^8.0.1" + "tar" "^6.0.2" + "unique-filename" "^1.1.1" + +"cacache@^15.2.0": + "integrity" "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==" + "resolved" "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" + "version" "15.3.0" + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + "chownr" "^2.0.0" + "fs-minipass" "^2.0.0" + "glob" "^7.1.4" + "infer-owner" "^1.0.4" + "lru-cache" "^6.0.0" + "minipass" "^3.1.1" + "minipass-collect" "^1.0.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.2" + "mkdirp" "^1.0.3" + "p-map" "^4.0.0" + "promise-inflight" "^1.0.1" + "rimraf" "^3.0.2" + "ssri" "^8.0.1" + "tar" "^6.0.2" + "unique-filename" "^1.1.1" + +"cache-base@^1.0.1": + "integrity" "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==" + "resolved" "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "collection-visit" "^1.0.0" + "component-emitter" "^1.2.1" + "get-value" "^2.0.6" + "has-value" "^1.0.0" + "isobject" "^3.0.1" + "set-value" "^2.0.0" + "to-object-path" "^0.3.0" + "union-value" "^1.0.0" + "unset-value" "^1.0.0" + +"cacheable-request@^6.0.0": + "integrity" "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==" + "resolved" "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" + "version" "6.1.0" + dependencies: + "clone-response" "^1.0.2" + "get-stream" "^5.1.0" + "http-cache-semantics" "^4.0.0" + "keyv" "^3.0.0" + "lowercase-keys" "^2.0.0" + "normalize-url" "^4.1.0" + "responselike" "^1.0.2" + +"call-bind@^1.0.0", "call-bind@^1.0.2": + "integrity" "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==" + "resolved" "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "function-bind" "^1.1.1" + "get-intrinsic" "^1.0.2" + +"callsites@^3.0.0": + "integrity" "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "resolved" "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + "version" "3.1.0" + +"camelcase@^5.0.0": + "integrity" "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "resolved" "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + "version" "5.3.1" + +"camelcase@^6.2.0": + "integrity" "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + "resolved" "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz" + "version" "6.2.0" + +"caniuse-api@^3.0.0": + "integrity" "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==" + "resolved" "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "browserslist" "^4.0.0" + "caniuse-lite" "^1.0.0" + "lodash.memoize" "^4.1.2" + "lodash.uniq" "^4.5.0" + +"caniuse-lite@^1.0.0", "caniuse-lite@^1.0.30000981", "caniuse-lite@^1.0.30001032", "caniuse-lite@^1.0.30001109", "caniuse-lite@^1.0.30001219": + "integrity" "sha512-nb8mDzfMdxBDN7ZKx8chWafAdBp5DAAlpWvNyUGe5tcDWd838zpzDN3Rah9cjCqhfOKkrvx40G2SDtP0qiWX/w==" + "resolved" "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001240.tgz" + "version" "1.0.30001240" + +"canonical-path@1.0.0": + "integrity" "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==" + "resolved" "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz" + "version" "1.0.0" + +"caseless@~0.12.0": + "integrity" "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "resolved" "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + "version" "0.12.0" + +"chalk@^2.0.0", "chalk@^2.4.2": + "integrity" "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + "version" "2.4.2" + dependencies: + "ansi-styles" "^3.2.1" + "escape-string-regexp" "^1.0.5" + "supports-color" "^5.3.0" + +"chalk@^4.1.0": + "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "ansi-styles" "^4.1.0" + "supports-color" "^7.1.0" + +"chalk@^4.1.2": + "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "ansi-styles" "^4.1.0" + "supports-color" "^7.1.0" + +"chardet@^0.7.0": + "integrity" "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + "resolved" "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + "version" "0.7.0" + +"chokidar@^2.1.8": + "integrity" "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==" + "resolved" "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz" + "version" "2.1.8" + dependencies: + "anymatch" "^2.0.0" + "async-each" "^1.0.1" + "braces" "^2.3.2" + "glob-parent" "^3.1.0" + "inherits" "^2.0.3" + "is-binary-path" "^1.0.0" + "is-glob" "^4.0.0" + "normalize-path" "^3.0.0" + "path-is-absolute" "^1.0.0" + "readdirp" "^2.2.1" + "upath" "^1.1.1" + optionalDependencies: + "fsevents" "^1.2.7" + +"chokidar@^3.0.0", "chokidar@^3.5.1", "chokidar@>=3.0.0 <4.0.0": + "integrity" "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==" + "resolved" "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz" + "version" "3.5.2" + dependencies: + "anymatch" "~3.1.2" + "braces" "~3.0.2" + "glob-parent" "~5.1.2" + "is-binary-path" "~2.1.0" + "is-glob" "~4.0.1" + "normalize-path" "~3.0.0" + "readdirp" "~3.6.0" + optionalDependencies: + "fsevents" "~2.3.2" + +"chownr@^2.0.0": + "integrity" "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + "resolved" "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + "version" "2.0.0" + +"chrome-trace-event@^1.0.2": + "integrity" "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" + "version" "1.0.3" + +"ci-info@^2.0.0": + "integrity" "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "resolved" "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" + "version" "2.0.0" + +"cint@^8.2.1": + "integrity" "sha1-cDhrG0jidz0NYxZqVa/5TvRFahI=" + "resolved" "https://registry.npmjs.org/cint/-/cint-8.2.1.tgz" + "version" "8.2.1" + +"circular-dependency-plugin@5.2.2": + "integrity" "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==" + "resolved" "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz" + "version" "5.2.2" + +"class-utils@^0.3.5": + "integrity" "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==" + "resolved" "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz" + "version" "0.3.6" + dependencies: + "arr-union" "^3.1.0" + "define-property" "^0.2.5" + "isobject" "^3.0.0" + "static-extend" "^0.1.1" + +"clean-stack@^2.0.0": + "integrity" "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + "resolved" "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + "version" "2.2.0" + +"cli-boxes@^2.2.1": + "integrity" "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + "resolved" "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz" + "version" "2.2.1" + +"cli-cursor@^3.1.0": + "integrity" "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==" + "resolved" "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "restore-cursor" "^3.1.0" + +"cli-spinners@^2.5.0": + "integrity" "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==" + "resolved" "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz" + "version" "2.6.0" + +"cli-table@^0.3.11": + "integrity" "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==" + "resolved" "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz" + "version" "0.3.11" + dependencies: + "colors" "1.0.3" + +"cli-width@^3.0.0": + "integrity" "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + "resolved" "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + "version" "3.0.0" + +"cliui@^5.0.0": + "integrity" "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==" + "resolved" "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "string-width" "^3.1.0" + "strip-ansi" "^5.2.0" + "wrap-ansi" "^5.1.0" + +"cliui@^7.0.2": + "integrity" "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==" + "resolved" "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + "version" "7.0.4" + dependencies: + "string-width" "^4.2.0" + "strip-ansi" "^6.0.0" + "wrap-ansi" "^7.0.0" + +"clone-deep@^4.0.1": + "integrity" "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==" + "resolved" "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "is-plain-object" "^2.0.4" + "kind-of" "^6.0.2" + "shallow-clone" "^3.0.0" + +"clone-response@^1.0.2": + "integrity" "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=" + "resolved" "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "mimic-response" "^1.0.0" + +"clone@^1.0.2": + "integrity" "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "resolved" "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + "version" "1.0.4" + +"code-point-at@^1.0.0": + "integrity" "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "resolved" "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" + "version" "1.1.0" + +"collection-visit@^1.0.0": + "integrity" "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=" + "resolved" "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "map-visit" "^1.0.0" + "object-visit" "^1.0.0" + +"color-convert@^1.9.0": + "integrity" "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==" + "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + "version" "1.9.3" + dependencies: + "color-name" "1.1.3" + +"color-convert@^2.0.1": + "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" + "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "color-name" "~1.1.4" + +"color-name@~1.1.4": + "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + "version" "1.1.4" + +"color-name@1.1.3": + "integrity" "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + "version" "1.1.3" + +"color-support@^1.1.2": + "integrity" "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + "resolved" "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + "version" "1.1.3" + +"colord@^2.0.1": + "integrity" "sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA==" + "resolved" "https://registry.npmjs.org/colord/-/colord-2.0.1.tgz" + "version" "2.0.1" + +"colorette@^1.2.1", "colorette@^1.2.2": + "integrity" "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + "resolved" "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz" + "version" "1.2.2" + +"colors@^1.4.0": + "integrity" "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + "resolved" "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" + "version" "1.4.0" + +"colors@1.0.3": + "integrity" "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + "resolved" "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz" + "version" "1.0.3" + +"combined-stream@^1.0.6", "combined-stream@~1.0.6": + "integrity" "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==" + "resolved" "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + "version" "1.0.8" + dependencies: + "delayed-stream" "~1.0.0" + +"commander@*", "commander@^7.1.0": + "integrity" "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "resolved" "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + "version" "7.2.0" + +"commander@^2.20.0": + "integrity" "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + "version" "2.20.3" + +"commander@^8.3.0": + "integrity" "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + "resolved" "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + "version" "8.3.0" + +"commander@2": + "integrity" "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + "version" "2.20.3" + +"commondir@^1.0.1": + "integrity" "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + "resolved" "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + "version" "1.0.1" + +"component-emitter@^1.2.1", "component-emitter@~1.3.0": + "integrity" "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "resolved" "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz" + "version" "1.3.0" + +"compressible@~2.0.16": + "integrity" "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==" + "resolved" "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + "version" "2.0.18" + dependencies: + "mime-db" ">= 1.43.0 < 2" + +"compression@^1.7.4": + "integrity" "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==" + "resolved" "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + "version" "1.7.4" + 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": + "integrity" "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "version" "0.0.1" + +"configstore@^5.0.1": + "integrity" "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==" + "resolved" "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "dot-prop" "^5.2.0" + "graceful-fs" "^4.1.2" + "make-dir" "^3.0.0" + "unique-string" "^2.0.0" + "write-file-atomic" "^3.0.0" + "xdg-basedir" "^4.0.0" + +"connect-history-api-fallback@^1.6.0": + "integrity" "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + "resolved" "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz" + "version" "1.6.0" + +"connect@^3.7.0": + "integrity" "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==" + "resolved" "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz" + "version" "3.7.0" + dependencies: + "debug" "2.6.9" + "finalhandler" "1.1.2" + "parseurl" "~1.3.3" + "utils-merge" "1.0.1" + +"console-control-strings@^1.0.0", "console-control-strings@^1.1.0", "console-control-strings@~1.1.0": + "integrity" "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "resolved" "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + "version" "1.1.0" + +"content-disposition@0.5.3": + "integrity" "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==" + "resolved" "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz" + "version" "0.5.3" + dependencies: + "safe-buffer" "5.1.2" + +"content-type@~1.0.4": + "integrity" "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "resolved" "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + "version" "1.0.4" + +"convert-source-map@^1.5.1", "convert-source-map@^1.7.0": + "integrity" "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==" + "resolved" "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" + "version" "1.8.0" + dependencies: + "safe-buffer" "~5.1.1" + +"cookie-signature@1.0.6": + "integrity" "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "resolved" "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + "version" "1.0.6" + +"cookie@~0.4.1": + "integrity" "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "resolved" "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" + "version" "0.4.1" + +"cookie@0.4.0": + "integrity" "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "resolved" "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" + "version" "0.4.0" + +"copy-anything@^2.0.1": + "integrity" "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==" + "resolved" "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz" + "version" "2.0.3" + dependencies: + "is-what" "^3.12.0" + +"copy-descriptor@^0.1.0": + "integrity" "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "resolved" "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz" + "version" "0.1.1" + +"copy-webpack-plugin@8.1.1": + "integrity" "sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==" + "resolved" "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz" + "version" "8.1.1" + dependencies: + "fast-glob" "^3.2.5" + "glob-parent" "^5.1.1" + "globby" "^11.0.3" + "normalize-path" "^3.0.0" + "p-limit" "^3.1.0" + "schema-utils" "^3.0.0" + "serialize-javascript" "^5.0.1" + +"core-js-compat@^3.14.0", "core-js-compat@^3.9.0": + "integrity" "sha512-xGhzYMX6y7oEGQGAJmP2TmtBLvR4nZmRGEcFa3ubHOq5YEp51gGN9AovVa0AoujGZIq+Wm6dISiYyGNfdflYww==" + "resolved" "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.15.1.tgz" + "version" "3.15.1" + dependencies: + "browserslist" "^4.16.6" + "semver" "7.0.0" + +"core-js@3.12.0": + "integrity" "sha512-SaMnchL//WwU2Ot1hhkPflE8gzo7uq1FGvUJ8GKmi3TOU7rGTHIU+eir1WGf6qOtTyxdfdcp10yPdGZ59sQ3hw==" + "resolved" "https://registry.npmjs.org/core-js/-/core-js-3.12.0.tgz" + "version" "3.12.0" + +"core-util-is@~1.0.0", "core-util-is@1.0.2": + "integrity" "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "resolved" "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + "version" "1.0.2" + +"cors@~2.8.5": + "integrity" "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==" + "resolved" "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + "version" "2.8.5" + dependencies: + "object-assign" "^4" + "vary" "^1" + +"cosmiconfig@^7.0.0": + "integrity" "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==" + "resolved" "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz" + "version" "7.0.0" + dependencies: + "@types/parse-json" "^4.0.0" + "import-fresh" "^3.2.1" + "parse-json" "^5.0.0" + "path-type" "^4.0.0" + "yaml" "^1.10.0" + +"critters@0.0.10": + "integrity" "sha512-p5VKhP1803+f+0Jq5P03w1SbiHtpAKm+1EpJHkiPxQPq0Vu9QLZHviJ02GRrWi0dlcJqrmzMWInbwp4d22RsGw==" + "resolved" "https://registry.npmjs.org/critters/-/critters-0.0.10.tgz" + "version" "0.0.10" + dependencies: + "chalk" "^4.1.0" + "css" "^3.0.0" + "parse5" "^6.0.1" + "parse5-htmlparser2-tree-adapter" "^6.0.1" + "pretty-bytes" "^5.3.0" + +"cross-spawn@^6.0.0": + "integrity" "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==" + "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" + "version" "6.0.5" + dependencies: + "nice-try" "^1.0.4" + "path-key" "^2.0.1" + "semver" "^5.5.0" + "shebang-command" "^1.2.0" + "which" "^1.2.9" + +"crypto-random-string@^2.0.0": + "integrity" "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + "resolved" "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" + "version" "2.0.0" + +"css-blank-pseudo@^0.1.4": + "integrity" "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==" + "resolved" "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz" + "version" "0.1.4" + dependencies: + "postcss" "^7.0.5" + +"css-color-names@^0.0.4": + "integrity" "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + "resolved" "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz" + "version" "0.0.4" + +"css-color-names@^1.0.1": + "integrity" "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==" + "resolved" "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz" + "version" "1.0.1" + +"css-declaration-sorter@^6.0.3": + "integrity" "sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==" + "resolved" "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz" + "version" "6.0.3" + dependencies: + "timsort" "^0.3.0" + +"css-has-pseudo@^0.10.0": + "integrity" "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==" + "resolved" "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz" + "version" "0.10.0" + dependencies: + "postcss" "^7.0.6" + "postcss-selector-parser" "^5.0.0-rc.4" + +"css-loader@5.2.4": + "integrity" "sha512-OFYGyINCKkdQsTrSYxzGSFnGS4gNjcXkKkQgWxK138jgnPt+lepxdjSZNc8sHAl5vP3DhsJUxufWIjOwI8PMMw==" + "resolved" "https://registry.npmjs.org/css-loader/-/css-loader-5.2.4.tgz" + "version" "5.2.4" + dependencies: + "camelcase" "^6.2.0" + "icss-utils" "^5.1.0" + "loader-utils" "^2.0.0" + "postcss" "^8.2.10" + "postcss-modules-extract-imports" "^3.0.0" + "postcss-modules-local-by-default" "^4.0.0" + "postcss-modules-scope" "^3.0.0" + "postcss-modules-values" "^4.0.0" + "postcss-value-parser" "^4.1.0" + "schema-utils" "^3.0.0" + "semver" "^7.3.5" + +"css-minimizer-webpack-plugin@3.0.0": + "integrity" "sha512-yIrqG0pPphR1RoNx2wDxYmxRf2ubRChLDXxv7ccipEm5bRKsZRYp8n+2peeXehtTF5s3yNxlqsdz3WQOsAgUkw==" + "resolved" "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "cssnano" "^5.0.0" + "jest-worker" "^26.3.0" + "p-limit" "^3.0.2" + "postcss" "^8.2.9" + "schema-utils" "^3.0.0" + "serialize-javascript" "^5.0.1" + "source-map" "^0.6.1" + +"css-parse@~2.0.0": + "integrity" "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=" + "resolved" "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "css" "^2.0.0" + +"css-prefers-color-scheme@^3.1.1": + "integrity" "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==" + "resolved" "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz" + "version" "3.1.1" + dependencies: + "postcss" "^7.0.5" + +"css-select@^4.1.3": + "integrity" "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==" + "resolved" "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz" + "version" "4.1.3" + dependencies: + "boolbase" "^1.0.0" + "css-what" "^5.0.0" + "domhandler" "^4.2.0" + "domutils" "^2.6.0" + "nth-check" "^2.0.0" + +"css-tree@^1.1.2": + "integrity" "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==" + "resolved" "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" + "version" "1.1.3" + dependencies: + "mdn-data" "2.0.14" + "source-map" "^0.6.1" + +"css-what@^5.0.0": + "integrity" "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" + "resolved" "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz" + "version" "5.0.1" + +"css@^2.0.0": + "integrity" "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==" + "resolved" "https://registry.npmjs.org/css/-/css-2.2.4.tgz" + "version" "2.2.4" + dependencies: + "inherits" "^2.0.3" + "source-map" "^0.6.1" + "source-map-resolve" "^0.5.2" + "urix" "^0.1.0" + +"css@^3.0.0": + "integrity" "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==" + "resolved" "https://registry.npmjs.org/css/-/css-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "inherits" "^2.0.4" + "source-map" "^0.6.1" + "source-map-resolve" "^0.6.0" + +"cssdb@^4.4.0": + "integrity" "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==" + "resolved" "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz" + "version" "4.4.0" + +"cssesc@^2.0.0": + "integrity" "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==" + "resolved" "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz" + "version" "2.0.0" + +"cssesc@^3.0.0": + "integrity" "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "resolved" "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + "version" "3.0.0" + +"cssnano-preset-default@^5.1.3": + "integrity" "sha512-qo9tX+t4yAAZ/yagVV3b+QBKeLklQbmgR3wI7mccrDcR+bEk9iHgZN1E7doX68y9ThznLya3RDmR+nc7l6/2WQ==" + "resolved" "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.3.tgz" + "version" "5.1.3" + dependencies: + "css-declaration-sorter" "^6.0.3" + "cssnano-utils" "^2.0.1" + "postcss-calc" "^8.0.0" + "postcss-colormin" "^5.2.0" + "postcss-convert-values" "^5.0.1" + "postcss-discard-comments" "^5.0.1" + "postcss-discard-duplicates" "^5.0.1" + "postcss-discard-empty" "^5.0.1" + "postcss-discard-overridden" "^5.0.1" + "postcss-merge-longhand" "^5.0.2" + "postcss-merge-rules" "^5.0.2" + "postcss-minify-font-values" "^5.0.1" + "postcss-minify-gradients" "^5.0.1" + "postcss-minify-params" "^5.0.1" + "postcss-minify-selectors" "^5.1.0" + "postcss-normalize-charset" "^5.0.1" + "postcss-normalize-display-values" "^5.0.1" + "postcss-normalize-positions" "^5.0.1" + "postcss-normalize-repeat-style" "^5.0.1" + "postcss-normalize-string" "^5.0.1" + "postcss-normalize-timing-functions" "^5.0.1" + "postcss-normalize-unicode" "^5.0.1" + "postcss-normalize-url" "^5.0.2" + "postcss-normalize-whitespace" "^5.0.1" + "postcss-ordered-values" "^5.0.2" + "postcss-reduce-initial" "^5.0.1" + "postcss-reduce-transforms" "^5.0.1" + "postcss-svgo" "^5.0.2" + "postcss-unique-selectors" "^5.0.1" + +"cssnano-utils@^2.0.1": + "integrity" "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" + "resolved" "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz" + "version" "2.0.1" + +"cssnano@^5.0.0": + "integrity" "sha512-NiaLH/7yqGksFGsFNvSRe2IV/qmEBAeDE64dYeD8OBrgp6lE8YoMeQJMtsv5ijo6MPyhuoOvFhI94reahBRDkw==" + "resolved" "https://registry.npmjs.org/cssnano/-/cssnano-5.0.6.tgz" + "version" "5.0.6" + dependencies: + "cosmiconfig" "^7.0.0" + "cssnano-preset-default" "^5.1.3" + "is-resolvable" "^1.1.0" + +"csso@^4.2.0": + "integrity" "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==" + "resolved" "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "css-tree" "^1.1.2" + +"custom-event@~1.0.0": + "integrity" "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" + "resolved" "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz" + "version" "1.0.1" + +"d3-array@^1.1.1", "d3-array@^1.2.0", "d3-array@1": + "integrity" "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + "resolved" "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz" + "version" "1.2.4" + +"d3-axis@1": + "integrity" "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + "resolved" "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz" + "version" "1.0.12" + +"d3-brush@1": + "integrity" "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==" + "resolved" "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz" + "version" "1.1.6" + dependencies: + "d3-dispatch" "1" + "d3-drag" "1" + "d3-interpolate" "1" + "d3-selection" "1" + "d3-transition" "1" + +"d3-chord@1": + "integrity" "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==" + "resolved" "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz" + "version" "1.0.6" + dependencies: + "d3-array" "1" + "d3-path" "1" + +"d3-collection@1": + "integrity" "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + "resolved" "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz" + "version" "1.0.7" + +"d3-color@1": + "integrity" "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + "resolved" "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz" + "version" "1.4.1" + +"d3-contour@1": + "integrity" "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==" + "resolved" "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "d3-array" "^1.1.1" + +"d3-dispatch@1": + "integrity" "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + "resolved" "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz" + "version" "1.0.6" + +"d3-drag@1": + "integrity" "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==" + "resolved" "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz" + "version" "1.2.5" + dependencies: + "d3-dispatch" "1" + "d3-selection" "1" + +"d3-dsv@1": + "integrity" "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==" + "resolved" "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "commander" "2" + "iconv-lite" "0.4" + "rw" "1" + +"d3-ease@1": + "integrity" "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + "resolved" "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz" + "version" "1.0.7" + +"d3-fetch@1": + "integrity" "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==" + "resolved" "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "d3-dsv" "1" + +"d3-force@1": + "integrity" "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==" + "resolved" "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz" + "version" "1.2.1" + dependencies: + "d3-collection" "1" + "d3-dispatch" "1" + "d3-quadtree" "1" + "d3-timer" "1" + +"d3-format@1": + "integrity" "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + "resolved" "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz" + "version" "1.4.5" + +"d3-geo@1": + "integrity" "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==" + "resolved" "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz" + "version" "1.12.1" + dependencies: + "d3-array" "1" + +"d3-hierarchy@1": + "integrity" "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + "resolved" "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz" + "version" "1.1.9" + +"d3-interpolate@1": + "integrity" "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==" + "resolved" "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "d3-color" "1" + +"d3-path@1": + "integrity" "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + "resolved" "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz" + "version" "1.0.9" + +"d3-polygon@1": + "integrity" "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + "resolved" "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz" + "version" "1.0.6" + +"d3-quadtree@1": + "integrity" "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + "resolved" "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz" + "version" "1.0.7" + +"d3-random@1": + "integrity" "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + "resolved" "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz" + "version" "1.1.2" + +"d3-scale-chromatic@1": + "integrity" "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==" + "resolved" "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz" + "version" "1.5.0" + dependencies: + "d3-color" "1" + "d3-interpolate" "1" + +"d3-scale@2": + "integrity" "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==" + "resolved" "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz" + "version" "2.2.2" + dependencies: + "d3-array" "^1.2.0" + "d3-collection" "1" + "d3-format" "1" + "d3-interpolate" "1" + "d3-time" "1" + "d3-time-format" "2" + +"d3-selection@^1.1.0", "d3-selection@1": + "integrity" "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + "resolved" "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz" + "version" "1.4.2" + +"d3-shape@1": + "integrity" "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==" + "resolved" "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz" + "version" "1.3.7" + dependencies: + "d3-path" "1" + +"d3-time-format@2": + "integrity" "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==" + "resolved" "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "d3-time" "1" + +"d3-time@1": + "integrity" "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + "resolved" "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz" + "version" "1.1.0" + +"d3-timer@1": + "integrity" "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + "resolved" "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz" + "version" "1.0.10" + +"d3-transition@1": + "integrity" "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==" + "resolved" "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "d3-color" "1" + "d3-dispatch" "1" + "d3-ease" "1" + "d3-interpolate" "1" + "d3-selection" "^1.1.0" + "d3-timer" "1" + +"d3-voronoi@1": + "integrity" "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + "resolved" "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz" + "version" "1.1.4" + +"d3-zoom@1": + "integrity" "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==" + "resolved" "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz" + "version" "1.8.3" + dependencies: + "d3-dispatch" "1" + "d3-drag" "1" + "d3-interpolate" "1" + "d3-selection" "1" + "d3-transition" "1" + +"d3@^5.14": + "integrity" "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==" + "resolved" "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz" + "version" "5.16.0" + dependencies: + "d3-array" "1" + "d3-axis" "1" + "d3-brush" "1" + "d3-chord" "1" + "d3-collection" "1" + "d3-color" "1" + "d3-contour" "1" + "d3-dispatch" "1" + "d3-drag" "1" + "d3-dsv" "1" + "d3-ease" "1" + "d3-fetch" "1" + "d3-force" "1" + "d3-format" "1" + "d3-geo" "1" + "d3-hierarchy" "1" + "d3-interpolate" "1" + "d3-path" "1" + "d3-polygon" "1" + "d3-quadtree" "1" + "d3-random" "1" + "d3-scale" "2" + "d3-scale-chromatic" "1" + "d3-selection" "1" + "d3-shape" "1" + "d3-time" "1" + "d3-time-format" "2" + "d3-timer" "1" + "d3-transition" "1" + "d3-voronoi" "1" + "d3-zoom" "1" + +"dagre-d3@^0.6.4": + "integrity" "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==" + "resolved" "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz" + "version" "0.6.4" + dependencies: + "d3" "^5.14" + "dagre" "^0.8.5" + "graphlib" "^2.1.8" + "lodash" "^4.17.15" + +"dagre@^0.8.5": + "integrity" "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==" + "resolved" "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz" + "version" "0.8.5" + dependencies: + "graphlib" "^2.1.8" + "lodash" "^4.17.15" + +"dashdash@^1.12.0": + "integrity" "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=" + "resolved" "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + "version" "1.14.1" + dependencies: + "assert-plus" "^1.0.0" + +"date-format@^2.1.0": + "integrity" "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" + "resolved" "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz" + "version" "2.1.0" + +"date-format@^3.0.0": + "integrity" "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==" + "resolved" "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz" + "version" "3.0.0" + +"debug@^2.2.0": + "integrity" "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" + "resolved" "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + "version" "2.6.9" + dependencies: + "ms" "2.0.0" + +"debug@^2.3.3": + "integrity" "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" + "resolved" "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + "version" "2.6.9" + dependencies: + "ms" "2.0.0" + +"debug@^3.1.1": + "integrity" "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==" + "resolved" "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + "version" "3.2.7" + dependencies: + "ms" "^2.1.1" + +"debug@^3.2.6": + "integrity" "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==" + "resolved" "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + "version" "3.2.7" + dependencies: + "ms" "^2.1.1" + +"debug@^4.1.0", "debug@^4.1.1", "debug@^4.3.1", "debug@~4.3.1", "debug@4", "debug@4.3.1": + "integrity" "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==" + "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz" + "version" "4.3.1" + dependencies: + "ms" "2.1.2" + +"debug@~3.1.0": + "integrity" "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==" + "resolved" "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "ms" "2.0.0" + +"debug@2.6.9": + "integrity" "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==" + "resolved" "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + "version" "2.6.9" + dependencies: + "ms" "2.0.0" + +"decamelize@^1.2.0": + "integrity" "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "resolved" "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + "version" "1.2.0" + +"decode-uri-component@^0.2.0": + "integrity" "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "resolved" "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" + "version" "0.2.0" + +"decompress-response@^3.3.0": + "integrity" "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=" + "resolved" "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" + "version" "3.3.0" + dependencies: + "mimic-response" "^1.0.0" + +"deep-equal@^1.0.1": + "integrity" "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==" + "resolved" "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "is-arguments" "^1.0.4" + "is-date-object" "^1.0.1" + "is-regex" "^1.0.4" + "object-is" "^1.0.1" + "object-keys" "^1.1.1" + "regexp.prototype.flags" "^1.2.0" + +"deep-extend@^0.6.0": + "integrity" "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "resolved" "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + "version" "0.6.0" + +"default-gateway@^4.2.0": + "integrity" "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==" + "resolved" "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "execa" "^1.0.0" + "ip-regex" "^2.1.0" + +"defaults@^1.0.3": + "integrity" "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=" + "resolved" "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "clone" "^1.0.2" + +"defer-to-connect@^1.0.1": + "integrity" "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "resolved" "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" + "version" "1.1.3" + +"define-lazy-prop@^2.0.0": + "integrity" "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + "resolved" "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + "version" "2.0.0" + +"define-properties@^1.1.3": + "integrity" "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==" + "resolved" "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" + "version" "1.1.3" + dependencies: + "object-keys" "^1.0.12" + +"define-property@^0.2.5": + "integrity" "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" + "resolved" "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz" + "version" "0.2.5" + dependencies: + "is-descriptor" "^0.1.0" + +"define-property@^1.0.0": + "integrity" "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" + "resolved" "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "is-descriptor" "^1.0.0" + +"define-property@^2.0.2": + "integrity" "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==" + "resolved" "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "is-descriptor" "^1.0.2" + "isobject" "^3.0.1" + +"del@^4.1.1": + "integrity" "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==" + "resolved" "https://registry.npmjs.org/del/-/del-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "@types/glob" "^7.1.1" + "globby" "^6.1.0" + "is-path-cwd" "^2.0.0" + "is-path-in-cwd" "^2.0.0" + "p-map" "^2.0.0" + "pify" "^4.0.1" + "rimraf" "^2.6.3" + +"delayed-stream@~1.0.0": + "integrity" "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "resolved" "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + "version" "1.0.0" + +"delegates@^1.0.0": + "integrity" "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "resolved" "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + "version" "1.0.0" + +"depd@^1.1.2", "depd@~1.1.2": + "integrity" "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "resolved" "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + "version" "1.1.2" + +"dependency-graph@^0.11.0": + "integrity" "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==" + "resolved" "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" + "version" "0.11.0" + +"destroy@~1.0.4": + "integrity" "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "resolved" "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + "version" "1.0.4" + +"detect-node@^2.0.4": + "integrity" "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + "resolved" "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + "version" "2.1.0" + +"di@^0.0.1": + "integrity" "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=" + "resolved" "https://registry.npmjs.org/di/-/di-0.0.1.tgz" + "version" "0.0.1" + +"dir-glob@^3.0.1": + "integrity" "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==" + "resolved" "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "path-type" "^4.0.0" + +"dns-equal@^1.0.0": + "integrity" "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + "resolved" "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" + "version" "1.0.0" + +"dns-packet@^1.3.1": + "integrity" "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==" + "resolved" "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz" + "version" "1.3.4" + dependencies: + "ip" "^1.1.0" + "safe-buffer" "^5.0.1" + +"dns-txt@^2.0.2": + "integrity" "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=" + "resolved" "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "buffer-indexof" "^1.0.0" + +"dom-serialize@^2.2.1": + "integrity" "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=" + "resolved" "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz" + "version" "2.2.1" + dependencies: + "custom-event" "~1.0.0" + "ent" "~2.2.0" + "extend" "^3.0.0" + "void-elements" "^2.0.0" + +"dom-serializer@^1.0.1": + "integrity" "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==" + "resolved" "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "domelementtype" "^2.0.1" + "domhandler" "^4.2.0" + "entities" "^2.0.0" + +"domelementtype@^2.0.1", "domelementtype@^2.2.0": + "integrity" "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + "resolved" "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz" + "version" "2.2.0" + +"domhandler@^4.2.0": + "integrity" "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==" + "resolved" "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "domelementtype" "^2.2.0" + +"domutils@^2.6.0": + "integrity" "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==" + "resolved" "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz" + "version" "2.7.0" + dependencies: + "dom-serializer" "^1.0.1" + "domelementtype" "^2.2.0" + "domhandler" "^4.2.0" + +"dot-prop@^5.2.0": + "integrity" "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==" + "resolved" "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" + "version" "5.3.0" + dependencies: + "is-obj" "^2.0.0" + +"duplexer@^0.1.1", "duplexer@~0.1.1": + "integrity" "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "resolved" "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" + "version" "0.1.2" + +"duplexer3@^0.1.4": + "integrity" "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "resolved" "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" + "version" "0.1.4" + +"ecc-jsbn@~0.1.1": + "integrity" "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=" + "resolved" "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + "version" "0.1.2" + dependencies: + "jsbn" "~0.1.0" + "safer-buffer" "^2.1.0" + +"ee-first@1.1.1": + "integrity" "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "resolved" "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + "version" "1.1.1" + +"electron-to-chromium@^1.3.723": + "integrity" "sha512-XPKwjX6pHezJWB4FLVuSil9gGmU6XYl27ahUwEHODXF4KjCEB8RuIT05MkU1au2Tdye57o49yY0uCMK+bwUt+A==" + "resolved" "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.760.tgz" + "version" "1.3.760" + +"emoji-regex@^7.0.1": + "integrity" "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz" + "version" "7.0.3" + +"emoji-regex@^8.0.0": + "integrity" "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "resolved" "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + "version" "8.0.0" + +"emojis-list@^3.0.0": + "integrity" "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + "resolved" "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + "version" "3.0.0" + +"encodeurl@~1.0.2": + "integrity" "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "resolved" "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + "version" "1.0.2" + +"encoding@^0.1.12": + "integrity" "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==" + "resolved" "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + "version" "0.1.13" + dependencies: + "iconv-lite" "^0.6.2" + +"end-of-stream@^1.1.0": + "integrity" "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==" + "resolved" "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + "version" "1.4.4" + dependencies: + "once" "^1.4.0" + +"engine.io-parser@~4.0.0": + "integrity" "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==" + "resolved" "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "base64-arraybuffer" "0.1.4" + +"engine.io@~4.1.0": + "integrity" "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==" + "resolved" "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "accepts" "~1.3.4" + "base64id" "2.0.0" + "cookie" "~0.4.1" + "cors" "~2.8.5" + "debug" "~4.3.1" + "engine.io-parser" "~4.0.0" + "ws" "~7.4.2" + +"enhanced-resolve@^5.8.0": + "integrity" "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==" + "resolved" "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz" + "version" "5.8.2" + dependencies: + "graceful-fs" "^4.2.4" + "tapable" "^2.2.0" + +"enhanced-resolve@5.7.0": + "integrity" "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==" + "resolved" "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz" + "version" "5.7.0" + dependencies: + "graceful-fs" "^4.2.4" + "tapable" "^2.2.0" + +"ent@~2.2.0": + "integrity" "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + "resolved" "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" + "version" "2.2.0" + +"entities@^2.0.0": + "integrity" "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + "resolved" "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" + "version" "2.2.0" + +"env-paths@^2.2.0": + "integrity" "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + "resolved" "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + "version" "2.2.1" + +"err-code@^2.0.2": + "integrity" "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + "resolved" "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" + "version" "2.0.3" + +"errno@^0.1.1", "errno@^0.1.3": + "integrity" "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==" + "resolved" "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz" + "version" "0.1.8" + dependencies: + "prr" "~1.0.1" + +"error-ex@^1.3.1": + "integrity" "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==" + "resolved" "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "is-arrayish" "^0.2.1" + +"es-module-lexer@^0.4.0": + "integrity" "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==" + "resolved" "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz" + "version" "0.4.1" + +"escalade@^3.1.1": + "integrity" "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "resolved" "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + "version" "3.1.1" + +"escape-goat@^2.0.0": + "integrity" "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + "resolved" "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz" + "version" "2.1.1" + +"escape-html@~1.0.3": + "integrity" "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "resolved" "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + "version" "1.0.3" + +"escape-string-regexp@^1.0.5": + "integrity" "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + "version" "1.0.5" + +"eslint-scope@5.1.1": + "integrity" "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==" + "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + "version" "5.1.1" + dependencies: + "esrecurse" "^4.3.0" + "estraverse" "^4.1.1" + +"esrecurse@^4.3.0": + "integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" + "resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "estraverse" "^5.2.0" + +"estraverse@^4.1.1": + "integrity" "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + "version" "4.3.0" + +"estraverse@^5.2.0": + "integrity" "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" + "version" "5.2.0" + +"esutils@^2.0.2": + "integrity" "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + "version" "2.0.3" + +"etag@~1.8.1": + "integrity" "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "resolved" "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + "version" "1.8.1" + +"event-stream@^4.0.1": + "integrity" "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==" + "resolved" "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "duplexer" "^0.1.1" + "from" "^0.1.7" + "map-stream" "0.0.7" + "pause-stream" "^0.0.11" + "split" "^1.0.1" + "stream-combiner" "^0.2.2" + "through" "^2.3.8" + +"eventemitter3@^4.0.0": + "integrity" "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "resolved" "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + "version" "4.0.7" + +"events@^3.2.0": + "integrity" "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + "resolved" "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + "version" "3.3.0" + +"eventsource@^1.0.7": + "integrity" "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==" + "resolved" "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "original" "^1.0.0" + +"execa@^1.0.0": + "integrity" "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==" + "resolved" "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz" + "version" "1.0.0" + 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" + +"expand-brackets@^2.1.4": + "integrity" "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=" + "resolved" "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz" + "version" "2.1.4" + dependencies: + "debug" "^2.3.3" + "define-property" "^0.2.5" + "extend-shallow" "^2.0.1" + "posix-character-classes" "^0.1.0" + "regex-not" "^1.0.0" + "snapdragon" "^0.8.1" + "to-regex" "^3.0.1" + +"express@^4.17.1": + "integrity" "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==" + "resolved" "https://registry.npmjs.org/express/-/express-4.17.1.tgz" + "version" "4.17.1" + 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-shallow@^2.0.1": + "integrity" "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=" + "resolved" "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "is-extendable" "^0.1.0" + +"extend-shallow@^3.0.0", "extend-shallow@^3.0.2": + "integrity" "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "resolved" "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "assign-symbols" "^1.0.0" + "is-extendable" "^1.0.1" + +"extend@^3.0.0", "extend@~3.0.2": + "integrity" "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "resolved" "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + "version" "3.0.2" + +"external-editor@^3.0.3": + "integrity" "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==" + "resolved" "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "chardet" "^0.7.0" + "iconv-lite" "^0.4.24" + "tmp" "^0.0.33" + +"extglob@^2.0.4": + "integrity" "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==" + "resolved" "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz" + "version" "2.0.4" + dependencies: + "array-unique" "^0.3.2" + "define-property" "^1.0.0" + "expand-brackets" "^2.1.4" + "extend-shallow" "^2.0.1" + "fragment-cache" "^0.2.1" + "regex-not" "^1.0.0" + "snapdragon" "^0.8.1" + "to-regex" "^3.0.1" + +"extsprintf@^1.2.0", "extsprintf@1.3.0": + "integrity" "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "resolved" "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + "version" "1.3.0" + +"fast-deep-equal@^3.1.1": + "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + "version" "3.1.3" + +"fast-glob@^3.1.1", "fast-glob@^3.2.5": + "integrity" "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==" + "resolved" "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz" + "version" "3.2.6" + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + "glob-parent" "^5.1.2" + "merge2" "^1.3.0" + "micromatch" "^4.0.4" + +"fast-json-stable-stringify@^2.0.0", "fast-json-stable-stringify@2.1.0": + "integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + "version" "2.1.0" + +"fast-memoize@^2.5.2": + "integrity" "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + "resolved" "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz" + "version" "2.5.2" + +"fastq@^1.6.0": + "integrity" "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==" + "resolved" "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz" + "version" "1.11.0" + dependencies: + "reusify" "^1.0.4" + +"faye-websocket@^0.11.3": + "integrity" "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==" + "resolved" "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + "version" "0.11.4" + dependencies: + "websocket-driver" ">=0.5.1" + +"figgy-pudding@^3.5.1": + "integrity" "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" + "resolved" "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz" + "version" "3.5.2" + +"figures@^3.0.0": + "integrity" "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==" + "resolved" "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" + "version" "3.2.0" + dependencies: + "escape-string-regexp" "^1.0.5" + +"file-saver@^2.0.5": + "integrity" "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + "resolved" "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz" + "version" "2.0.5" + +"file-uri-to-path@1.0.0": + "integrity" "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "resolved" "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + "version" "1.0.0" + +"fill-range@^4.0.0": + "integrity" "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=" + "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "extend-shallow" "^2.0.1" + "is-number" "^3.0.0" + "repeat-string" "^1.6.1" + "to-regex-range" "^2.1.0" + +"fill-range@^7.0.1": + "integrity" "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==" + "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + "version" "7.0.1" + dependencies: + "to-regex-range" "^5.0.1" + +"finalhandler@~1.1.2", "finalhandler@1.1.2": + "integrity" "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==" + "resolved" "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + "version" "1.1.2" + 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" + +"find-cache-dir@^3.3.1", "find-cache-dir@3.3.1": + "integrity" "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==" + "resolved" "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz" + "version" "3.3.1" + dependencies: + "commondir" "^1.0.1" + "make-dir" "^3.0.2" + "pkg-dir" "^4.1.0" + +"find-up@^3.0.0": + "integrity" "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==" + "resolved" "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "locate-path" "^3.0.0" + +"find-up@^4.0.0": + "integrity" "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==" + "resolved" "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "locate-path" "^5.0.0" + "path-exists" "^4.0.0" + +"find-up@5.0.0": + "integrity" "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==" + "resolved" "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "locate-path" "^6.0.0" + "path-exists" "^4.0.0" + +"flatted@^2.0.1": + "integrity" "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" + "resolved" "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz" + "version" "2.0.2" + +"flatten@^1.0.2": + "integrity" "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==" + "resolved" "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz" + "version" "1.0.3" + +"follow-redirects@^1.0.0": + "integrity" "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz" + "version" "1.14.1" + +"for-in@^1.0.2": + "integrity" "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "resolved" "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" + "version" "1.0.2" + +"forever-agent@~0.6.1": + "integrity" "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "resolved" "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + "version" "0.6.1" + +"form-data@~2.3.2": + "integrity" "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==" + "resolved" "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + "version" "2.3.3" + dependencies: + "asynckit" "^0.4.0" + "combined-stream" "^1.0.6" + "mime-types" "^2.1.12" + +"forwarded@0.2.0": + "integrity" "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "resolved" "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + "version" "0.2.0" + +"fp-and-or@^0.1.3": + "integrity" "sha512-wJaE62fLaB3jCYvY2ZHjZvmKK2iiLiiehX38rz5QZxtdN8fVPJDeZUiVvJrHStdTc+23LHlyZuSEKgFc0pxi2g==" + "resolved" "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.3.tgz" + "version" "0.1.3" + +"fragment-cache@^0.2.1": + "integrity" "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=" + "resolved" "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz" + "version" "0.2.1" + dependencies: + "map-cache" "^0.2.2" + +"fresh@0.5.2": + "integrity" "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "resolved" "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + "version" "0.5.2" + +"from@^0.1.7": + "integrity" "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + "resolved" "https://registry.npmjs.org/from/-/from-0.1.7.tgz" + "version" "0.1.7" + +"fs-extra@^8.1.0": + "integrity" "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==" + "resolved" "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" + "version" "8.1.0" + dependencies: + "graceful-fs" "^4.2.0" + "jsonfile" "^4.0.0" + "universalify" "^0.1.0" + +"fs-minipass@^2.0.0", "fs-minipass@^2.1.0": + "integrity" "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==" + "resolved" "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "minipass" "^3.0.0" + +"fs-monkey@1.0.3": + "integrity" "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" + "resolved" "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" + "version" "1.0.3" + +"fs.realpath@^1.0.0": + "integrity" "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + "version" "1.0.0" + +"fsevents@^1.2.7": + "integrity" "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==" + "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz" + "version" "1.2.13" + dependencies: + "bindings" "^1.5.0" + "nan" "^2.12.1" + +"fsevents@~2.3.2": + "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + "version" "2.3.2" + +"function-bind@^1.1.1": + "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + "version" "1.1.1" + +"gauge@^4.0.0": + "integrity" "sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw==" + "resolved" "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "ansi-regex" "^5.0.1" + "aproba" "^1.0.3 || ^2.0.0" + "color-support" "^1.1.2" + "console-control-strings" "^1.0.0" + "has-unicode" "^2.0.1" + "signal-exit" "^3.0.0" + "string-width" "^4.2.3" + "strip-ansi" "^6.0.1" + "wide-align" "^1.1.2" + +"gauge@~2.7.3": + "integrity" "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=" + "resolved" "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz" + "version" "2.7.4" + dependencies: + "aproba" "^1.0.3" + "console-control-strings" "^1.0.0" + "has-unicode" "^2.0.0" + "object-assign" "^4.1.0" + "signal-exit" "^3.0.0" + "string-width" "^1.0.1" + "strip-ansi" "^3.0.1" + "wide-align" "^1.1.0" + +"gensync@^1.0.0-beta.1", "gensync@^1.0.0-beta.2": + "integrity" "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "resolved" "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + "version" "1.0.0-beta.2" + +"get-caller-file@^2.0.1", "get-caller-file@^2.0.5": + "integrity" "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "resolved" "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + "version" "2.0.5" + +"get-intrinsic@^1.0.2": + "integrity" "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==" + "resolved" "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "function-bind" "^1.1.1" + "has" "^1.0.3" + "has-symbols" "^1.0.1" + +"get-stdin@^8.0.0": + "integrity" "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==" + "resolved" "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz" + "version" "8.0.0" + +"get-stream@^4.0.0", "get-stream@^4.1.0": + "integrity" "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==" + "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "pump" "^3.0.0" + +"get-stream@^5.1.0": + "integrity" "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==" + "resolved" "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "pump" "^3.0.0" + +"get-value@^2.0.3", "get-value@^2.0.6": + "integrity" "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "resolved" "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" + "version" "2.0.6" + +"getpass@^0.1.1": + "integrity" "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=" + "resolved" "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + "version" "0.1.7" + dependencies: + "assert-plus" "^1.0.0" + +"glob-parent@^3.1.0": + "integrity" "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=" + "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "is-glob" "^3.1.0" + "path-dirname" "^1.0.0" + +"glob-parent@^5.1.1", "glob-parent@^5.1.2", "glob-parent@~5.1.2": + "integrity" "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==" + "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "is-glob" "^4.0.1" + +"glob-to-regexp@^0.4.1": + "integrity" "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "resolved" "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + "version" "0.4.1" + +"glob@^7.0.3", "glob@^7.1.3", "glob@^7.1.4", "glob@^7.1.6", "glob@^7.1.7", "glob@7.1.7": + "integrity" "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==" + "resolved" "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" + "version" "7.1.7" + 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" + +"global-dirs@^3.0.0": + "integrity" "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==" + "resolved" "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "ini" "2.0.0" + +"globals@^11.1.0": + "integrity" "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "resolved" "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + "version" "11.12.0" + +"globby@^11.0.3", "globby@^11.0.4": + "integrity" "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==" + "resolved" "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz" + "version" "11.0.4" + dependencies: + "array-union" "^2.1.0" + "dir-glob" "^3.0.1" + "fast-glob" "^3.1.1" + "ignore" "^5.1.4" + "merge2" "^1.3.0" + "slash" "^3.0.0" + +"globby@^6.1.0": + "integrity" "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=" + "resolved" "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz" + "version" "6.1.0" + dependencies: + "array-union" "^1.0.1" + "glob" "^7.0.3" + "object-assign" "^4.0.1" + "pify" "^2.0.0" + "pinkie-promise" "^2.0.0" + +"got@^9.6.0": + "integrity" "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==" + "resolved" "https://registry.npmjs.org/got/-/got-9.6.0.tgz" + "version" "9.6.0" + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + "cacheable-request" "^6.0.0" + "decompress-response" "^3.3.0" + "duplexer3" "^0.1.4" + "get-stream" "^4.1.0" + "lowercase-keys" "^1.0.1" + "mimic-response" "^1.0.1" + "p-cancelable" "^1.0.0" + "to-readable-stream" "^1.0.0" + "url-parse-lax" "^3.0.0" + +"graceful-fs@^4.1.11", "graceful-fs@^4.1.2", "graceful-fs@^4.1.6", "graceful-fs@^4.2.0", "graceful-fs@^4.2.3", "graceful-fs@^4.2.4", "graceful-fs@^4.2.6": + "integrity" "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "resolved" "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz" + "version" "4.2.6" + +"graphlib@^2.1.8": + "integrity" "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==" + "resolved" "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz" + "version" "2.1.8" + dependencies: + "lodash" "^4.17.15" + +"hammerjs@^2.0.8": + "integrity" "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" + "resolved" "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz" + "version" "2.0.8" + +"handle-thing@^2.0.0": + "integrity" "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + "resolved" "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + "version" "2.0.1" + +"har-schema@^2.0.0": + "integrity" "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "resolved" "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + "version" "2.0.0" + +"har-validator@~5.1.3": + "integrity" "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==" + "resolved" "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + "version" "5.1.5" + dependencies: + "ajv" "^6.12.3" + "har-schema" "^2.0.0" + +"has-flag@^3.0.0": + "integrity" "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + "version" "3.0.0" + +"has-flag@^4.0.0": + "integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + "version" "4.0.0" + +"has-symbols@^1.0.1", "has-symbols@^1.0.2": + "integrity" "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + "resolved" "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" + "version" "1.0.2" + +"has-unicode@^2.0.0", "has-unicode@^2.0.1": + "integrity" "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "resolved" "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + "version" "2.0.1" + +"has-value@^0.3.1": + "integrity" "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=" + "resolved" "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" + "version" "0.3.1" + dependencies: + "get-value" "^2.0.3" + "has-values" "^0.1.4" + "isobject" "^2.0.0" + +"has-value@^1.0.0": + "integrity" "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=" + "resolved" "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "get-value" "^2.0.6" + "has-values" "^1.0.0" + "isobject" "^3.0.0" + +"has-values@^0.1.4": + "integrity" "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "resolved" "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz" + "version" "0.1.4" + +"has-values@^1.0.0": + "integrity" "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=" + "resolved" "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "is-number" "^3.0.0" + "kind-of" "^4.0.0" + +"has-yarn@^2.1.0": + "integrity" "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + "resolved" "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz" + "version" "2.1.0" + +"has@^1.0.3": + "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==" + "resolved" "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "function-bind" "^1.1.1" + +"hex-color-regex@^1.1.0": + "integrity" "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + "resolved" "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz" + "version" "1.1.0" + +"hosted-git-info@^4.0.1", "hosted-git-info@^4.0.2": + "integrity" "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==" + "resolved" "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "lru-cache" "^6.0.0" + +"hpack.js@^2.1.6": + "integrity" "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=" + "resolved" "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + "version" "2.1.6" + dependencies: + "inherits" "^2.0.1" + "obuf" "^1.0.0" + "readable-stream" "^2.0.1" + "wbuf" "^1.1.0" + +"hsl-regex@^1.0.0": + "integrity" "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + "resolved" "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz" + "version" "1.0.0" + +"hsla-regex@^1.0.0": + "integrity" "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + "resolved" "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz" + "version" "1.0.0" + +"html-entities@^1.3.1": + "integrity" "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" + "resolved" "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz" + "version" "1.4.0" + +"html-escaper@^2.0.0": + "integrity" "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "resolved" "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + "version" "2.0.2" + +"http-cache-semantics@^4.0.0", "http-cache-semantics@^4.1.0": + "integrity" "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "resolved" "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" + "version" "4.1.0" + +"http-deceiver@^1.2.7": + "integrity" "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "resolved" "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + "version" "1.2.7" + +"http-errors@~1.6.2": + "integrity" "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=" + "resolved" "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + "version" "1.6.3" + dependencies: + "depd" "~1.1.2" + "inherits" "2.0.3" + "setprototypeof" "1.1.0" + "statuses" ">= 1.4.0 < 2" + +"http-errors@~1.7.2", "http-errors@1.7.2": + "integrity" "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==" + "resolved" "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz" + "version" "1.7.2" + dependencies: + "depd" "~1.1.2" + "inherits" "2.0.3" + "setprototypeof" "1.1.1" + "statuses" ">= 1.5.0 < 2" + "toidentifier" "1.0.0" + +"http-parser-js@>=0.5.1": + "integrity" "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" + "resolved" "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz" + "version" "0.5.3" + +"http-proxy-agent@^4.0.1": + "integrity" "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==" + "resolved" "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "@tootallnate/once" "1" + "agent-base" "6" + "debug" "4" + +"http-proxy-middleware@0.19.1": + "integrity" "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==" + "resolved" "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz" + "version" "0.19.1" + dependencies: + "http-proxy" "^1.17.0" + "is-glob" "^4.0.0" + "lodash" "^4.17.11" + "micromatch" "^3.1.10" + +"http-proxy@^1.17.0", "http-proxy@^1.18.1": + "integrity" "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==" + "resolved" "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + "version" "1.18.1" + dependencies: + "eventemitter3" "^4.0.0" + "follow-redirects" "^1.0.0" + "requires-port" "^1.0.0" + +"http-signature@~1.2.0": + "integrity" "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=" + "resolved" "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "assert-plus" "^1.0.0" + "jsprim" "^1.2.2" + "sshpk" "^1.7.0" + +"https-proxy-agent@^5.0.0", "https-proxy-agent@5.0.0": + "integrity" "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==" + "resolved" "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "agent-base" "6" + "debug" "4" + +"humanize-ms@^1.2.1": + "integrity" "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=" + "resolved" "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + "version" "1.2.1" + dependencies: + "ms" "^2.0.0" + +"iconv-lite@^0.4.24", "iconv-lite@^0.4.4", "iconv-lite@0.4", "iconv-lite@0.4.24": + "integrity" "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==" + "resolved" "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + "version" "0.4.24" + dependencies: + "safer-buffer" ">= 2.1.2 < 3" + +"iconv-lite@^0.6.2": + "integrity" "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==" + "resolved" "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + "version" "0.6.3" + dependencies: + "safer-buffer" ">= 2.1.2 < 3.0.0" + +"icss-utils@^5.0.0", "icss-utils@^5.1.0": + "integrity" "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" + "resolved" "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" + "version" "5.1.0" + +"ieee754@^1.1.13": + "integrity" "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "resolved" "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + "version" "1.2.1" + +"ignore-walk@^3.0.3": + "integrity" "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==" + "resolved" "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "minimatch" "^3.0.4" + +"ignore-walk@^4.0.1": + "integrity" "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==" + "resolved" "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "minimatch" "^3.0.4" + +"ignore@^5.1.4": + "integrity" "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz" + "version" "5.1.8" + +"image-size@~0.5.0": + "integrity" "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=" + "resolved" "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz" + "version" "0.5.5" + +"import-fresh@^3.2.1": + "integrity" "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==" + "resolved" "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + "version" "3.3.0" + dependencies: + "parent-module" "^1.0.0" + "resolve-from" "^4.0.0" + +"import-lazy@^2.1.0": + "integrity" "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "resolved" "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz" + "version" "2.1.0" + +"import-local@^2.0.0": + "integrity" "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==" + "resolved" "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "pkg-dir" "^3.0.0" + "resolve-cwd" "^2.0.0" + +"imurmurhash@^0.1.4": + "integrity" "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + "version" "0.1.4" + +"indent-string@^4.0.0": + "integrity" "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + "resolved" "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + "version" "4.0.0" + +"indexes-of@^1.0.1": + "integrity" "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + "resolved" "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz" + "version" "1.0.1" + +"infer-owner@^1.0.4": + "integrity" "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "resolved" "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + "version" "1.0.4" + +"inflight@^1.0.4": + "integrity" "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + "version" "1.0.6" + dependencies: + "once" "^1.3.0" + "wrappy" "1" + +"inherits@^2.0.1", "inherits@^2.0.3", "inherits@^2.0.4", "inherits@~2.0.3", "inherits@2": + "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + "version" "2.0.4" + +"inherits@2.0.3": + "integrity" "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + "version" "2.0.3" + +"ini@^1.3.5": + "integrity" "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "resolved" "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + "version" "1.3.8" + +"ini@~1.3.0": + "integrity" "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "resolved" "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + "version" "1.3.8" + +"ini@2.0.0": + "integrity" "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" + "resolved" "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" + "version" "2.0.0" + +"inquirer@8.0.0": + "integrity" "sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==" + "resolved" "https://registry.npmjs.org/inquirer/-/inquirer-8.0.0.tgz" + "version" "8.0.0" + dependencies: + "ansi-escapes" "^4.2.1" + "chalk" "^4.1.0" + "cli-cursor" "^3.1.0" + "cli-width" "^3.0.0" + "external-editor" "^3.0.3" + "figures" "^3.0.0" + "lodash" "^4.17.21" + "mute-stream" "0.0.8" + "run-async" "^2.4.0" + "rxjs" "^6.6.6" + "string-width" "^4.1.0" + "strip-ansi" "^6.0.0" + "through" "^2.3.6" + +"internal-ip@^4.3.0": + "integrity" "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==" + "resolved" "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "default-gateway" "^4.2.0" + "ipaddr.js" "^1.9.0" + +"ip-regex@^2.1.0": + "integrity" "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + "resolved" "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz" + "version" "2.1.0" + +"ip@^1.1.0", "ip@^1.1.5": + "integrity" "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "resolved" "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" + "version" "1.1.5" + +"ipaddr.js@^1.9.0", "ipaddr.js@1.9.1": + "integrity" "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "resolved" "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + "version" "1.9.1" + +"is-absolute-url@^3.0.3": + "integrity" "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" + "resolved" "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz" + "version" "3.0.3" + +"is-accessor-descriptor@^0.1.6": + "integrity" "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=" + "resolved" "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz" + "version" "0.1.6" + dependencies: + "kind-of" "^3.0.2" + +"is-accessor-descriptor@^1.0.0": + "integrity" "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==" + "resolved" "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "kind-of" "^6.0.0" + +"is-arguments@^1.0.4": + "integrity" "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==" + "resolved" "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "call-bind" "^1.0.0" + +"is-arrayish@^0.2.1": + "integrity" "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "resolved" "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + "version" "0.2.1" + +"is-binary-path@^1.0.0": + "integrity" "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=" + "resolved" "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "binary-extensions" "^1.0.0" + +"is-binary-path@~2.1.0": + "integrity" "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==" + "resolved" "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "binary-extensions" "^2.0.0" + +"is-buffer@^1.1.5": + "integrity" "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "resolved" "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + "version" "1.1.6" + +"is-ci@^2.0.0": + "integrity" "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==" + "resolved" "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "ci-info" "^2.0.0" + +"is-color-stop@^1.1.0": + "integrity" "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=" + "resolved" "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "css-color-names" "^0.0.4" + "hex-color-regex" "^1.1.0" + "hsl-regex" "^1.0.0" + "hsla-regex" "^1.0.0" + "rgb-regex" "^1.0.1" + "rgba-regex" "^1.0.0" + +"is-core-module@^2.2.0": + "integrity" "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==" + "resolved" "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz" + "version" "2.4.0" + dependencies: + "has" "^1.0.3" + +"is-data-descriptor@^0.1.4": + "integrity" "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=" + "resolved" "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" + "version" "0.1.4" + dependencies: + "kind-of" "^3.0.2" + +"is-data-descriptor@^1.0.0": + "integrity" "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==" + "resolved" "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "kind-of" "^6.0.0" + +"is-date-object@^1.0.1": + "integrity" "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" + "resolved" "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz" + "version" "1.0.4" + +"is-descriptor@^0.1.0": + "integrity" "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==" + "resolved" "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz" + "version" "0.1.6" + dependencies: + "is-accessor-descriptor" "^0.1.6" + "is-data-descriptor" "^0.1.4" + "kind-of" "^5.0.0" + +"is-descriptor@^1.0.0": + "integrity" "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==" + "resolved" "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "is-accessor-descriptor" "^1.0.0" + "is-data-descriptor" "^1.0.0" + "kind-of" "^6.0.2" + +"is-descriptor@^1.0.2": + "integrity" "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==" + "resolved" "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "is-accessor-descriptor" "^1.0.0" + "is-data-descriptor" "^1.0.0" + "kind-of" "^6.0.2" + +"is-docker@^2.0.0", "is-docker@^2.1.1": + "integrity" "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + "resolved" "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + "version" "2.2.1" + +"is-extendable@^0.1.0", "is-extendable@^0.1.1": + "integrity" "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "resolved" "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + "version" "0.1.1" + +"is-extendable@^1.0.1": + "integrity" "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==" + "resolved" "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "is-plain-object" "^2.0.4" + +"is-extglob@^2.1.0", "is-extglob@^2.1.1": + "integrity" "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + "version" "2.1.1" + +"is-fullwidth-code-point@^1.0.0": + "integrity" "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=" + "resolved" "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "number-is-nan" "^1.0.0" + +"is-fullwidth-code-point@^2.0.0": + "integrity" "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "resolved" "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" + "version" "2.0.0" + +"is-fullwidth-code-point@^3.0.0": + "integrity" "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "resolved" "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + "version" "3.0.0" + +"is-glob@^3.1.0": + "integrity" "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=" + "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "is-extglob" "^2.1.0" + +"is-glob@^4.0.0", "is-glob@^4.0.1", "is-glob@~4.0.1": + "integrity" "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==" + "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "is-extglob" "^2.1.1" + +"is-installed-globally@^0.4.0": + "integrity" "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==" + "resolved" "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz" + "version" "0.4.0" + dependencies: + "global-dirs" "^3.0.0" + "is-path-inside" "^3.0.2" + +"is-interactive@^1.0.0": + "integrity" "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + "resolved" "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" + "version" "1.0.0" + +"is-lambda@^1.0.1": + "integrity" "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + "resolved" "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" + "version" "1.0.1" + +"is-npm@^5.0.0": + "integrity" "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" + "resolved" "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz" + "version" "5.0.0" + +"is-number@^3.0.0": + "integrity" "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=" + "resolved" "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "kind-of" "^3.0.2" + +"is-number@^7.0.0": + "integrity" "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "resolved" "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + "version" "7.0.0" + +"is-obj@^2.0.0": + "integrity" "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + "resolved" "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" + "version" "2.0.0" + +"is-path-cwd@^2.0.0": + "integrity" "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" + "resolved" "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" + "version" "2.2.0" + +"is-path-in-cwd@^2.0.0": + "integrity" "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==" + "resolved" "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "is-path-inside" "^2.1.0" + +"is-path-inside@^2.1.0": + "integrity" "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==" + "resolved" "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "path-is-inside" "^1.0.2" + +"is-path-inside@^3.0.2": + "integrity" "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + "resolved" "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + "version" "3.0.3" + +"is-plain-object@^2.0.3", "is-plain-object@^2.0.4": + "integrity" "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==" + "resolved" "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + "version" "2.0.4" + dependencies: + "isobject" "^3.0.1" + +"is-regex@^1.0.4": + "integrity" "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==" + "resolved" "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz" + "version" "1.1.3" + dependencies: + "call-bind" "^1.0.2" + "has-symbols" "^1.0.2" + +"is-resolvable@^1.1.0": + "integrity" "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + "resolved" "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz" + "version" "1.1.0" + +"is-stream@^1.1.0": + "integrity" "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "resolved" "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + "version" "1.1.0" + +"is-typedarray@^1.0.0", "is-typedarray@~1.0.0": + "integrity" "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "resolved" "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + "version" "1.0.0" + +"is-unicode-supported@^0.1.0": + "integrity" "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + "resolved" "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + "version" "0.1.0" + +"is-what@^3.12.0": + "integrity" "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==" + "resolved" "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz" + "version" "3.14.1" + +"is-windows@^1.0.2": + "integrity" "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "resolved" "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" + "version" "1.0.2" + +"is-wsl@^1.1.0": + "integrity" "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + "resolved" "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz" + "version" "1.1.0" + +"is-wsl@^2.2.0": + "integrity" "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==" + "resolved" "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + "version" "2.2.0" + dependencies: + "is-docker" "^2.0.0" + +"is-yarn-global@^0.3.0": + "integrity" "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + "resolved" "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz" + "version" "0.3.0" + +"isarray@~1.0.0", "isarray@1.0.0": + "integrity" "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "resolved" "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + "version" "1.0.0" + +"isbinaryfile@^4.0.8": + "integrity" "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==" + "resolved" "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz" + "version" "4.0.8" + +"isexe@^2.0.0": + "integrity" "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + "version" "2.0.0" + +"isobject@^2.0.0": + "integrity" "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=" + "resolved" "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "isarray" "1.0.0" + +"isobject@^3.0.0", "isobject@^3.0.1": + "integrity" "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "resolved" "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + "version" "3.0.1" + +"isstream@~0.1.2": + "integrity" "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "resolved" "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + "version" "0.1.2" + +"istanbul-lib-coverage@^3.0.0": + "integrity" "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + "resolved" "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz" + "version" "3.0.0" + +"istanbul-lib-instrument@^4.0.1", "istanbul-lib-instrument@^4.0.3": + "integrity" "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==" + "resolved" "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz" + "version" "4.0.3" + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + "istanbul-lib-coverage" "^3.0.0" + "semver" "^6.3.0" + +"istanbul-lib-report@^3.0.0": + "integrity" "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==" + "resolved" "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "istanbul-lib-coverage" "^3.0.0" + "make-dir" "^3.0.0" + "supports-color" "^7.1.0" + +"istanbul-lib-source-maps@^4.0.0": + "integrity" "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==" + "resolved" "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "debug" "^4.1.1" + "istanbul-lib-coverage" "^3.0.0" + "source-map" "^0.6.1" + +"istanbul-reports@^3.0.0": + "integrity" "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==" + "resolved" "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "html-escaper" "^2.0.0" + "istanbul-lib-report" "^3.0.0" + +"jasmine-core@^3.6.0", "jasmine-core@>=3.7.1", "jasmine-core@~3.7.0": + "integrity" "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==" + "resolved" "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz" + "version" "3.7.1" + +"jest-worker@^26.3.0", "jest-worker@^26.6.2", "jest-worker@26.6.2": + "integrity" "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==" + "resolved" "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" + "version" "26.6.2" + dependencies: + "@types/node" "*" + "merge-stream" "^2.0.0" + "supports-color" "^7.0.0" + +"jju@^1.1.0": + "integrity" "sha1-o6vicYryQaKykE+EpiWXDzia4yo=" + "resolved" "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz" + "version" "1.4.0" + +"jquery@^3.6.0", "jquery@1.9.1 - 3": + "integrity" "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "resolved" "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz" + "version" "3.6.0" + +"js-tokens@^4.0.0": + "integrity" "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "resolved" "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + "version" "4.0.0" + +"js-yaml@^4.0.0": + "integrity" "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" + "resolved" "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "argparse" "^2.0.1" + +"jsbn@~0.1.0": + "integrity" "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "resolved" "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + "version" "0.1.1" + +"jsesc@^2.5.1": + "integrity" "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "resolved" "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + "version" "2.5.2" + +"jsesc@~0.5.0": + "integrity" "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "resolved" "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + "version" "0.5.0" + +"json-buffer@3.0.0": + "integrity" "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "resolved" "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" + "version" "3.0.0" + +"json-parse-better-errors@^1.0.2": + "integrity" "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "resolved" "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" + "version" "1.0.2" + +"json-parse-even-better-errors@^2.3.0": + "integrity" "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "resolved" "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + "version" "2.3.1" + +"json-parse-helpfulerror@^1.0.3": + "integrity" "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=" + "resolved" "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "jju" "^1.1.0" + +"json-schema-traverse@^0.4.1": + "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + "version" "0.4.1" + +"json-schema-traverse@^1.0.0": + "integrity" "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + "version" "1.0.0" + +"json-schema@0.2.3": + "integrity" "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "resolved" "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" + "version" "0.2.3" + +"json-stringify-safe@~5.0.1": + "integrity" "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "resolved" "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + "version" "5.0.1" + +"json3@^3.3.3": + "integrity" "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==" + "resolved" "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz" + "version" "3.3.3" + +"json5@^1.0.1": + "integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" + "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "minimist" "^1.2.0" + +"json5@^2.1.0", "json5@^2.1.2": + "integrity" "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==" + "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz" + "version" "2.2.0" + dependencies: + "minimist" "^1.2.5" + +"jsonc-parser@3.0.0": + "integrity" "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + "resolved" "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz" + "version" "3.0.0" + +"jsonfile@^4.0.0": + "integrity" "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=" + "resolved" "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" + "version" "4.0.0" + optionalDependencies: + "graceful-fs" "^4.1.6" + +"jsonlines@^0.1.1": + "integrity" "sha1-T80kbcXQ44aRkHxEqwAveC0dlMw=" + "resolved" "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz" + "version" "0.1.1" + +"jsonparse@^1.3.1": + "integrity" "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "resolved" "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + "version" "1.3.1" + +"jsprim@^1.2.2": + "integrity" "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=" + "resolved" "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" + "version" "1.4.1" + dependencies: + "assert-plus" "1.0.0" + "extsprintf" "1.3.0" + "json-schema" "0.2.3" + "verror" "1.10.0" + +"jwt-decode@^3.1.2": + "integrity" "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + "resolved" "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz" + "version" "3.1.2" + +"karma-chrome-launcher@~3.1.0": + "integrity" "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==" + "resolved" "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "which" "^1.2.1" + +"karma-coverage@~2.0.3": + "integrity" "sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==" + "resolved" "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.0.3.tgz" + "version" "2.0.3" + dependencies: + "istanbul-lib-coverage" "^3.0.0" + "istanbul-lib-instrument" "^4.0.1" + "istanbul-lib-report" "^3.0.0" + "istanbul-lib-source-maps" "^4.0.0" + "istanbul-reports" "^3.0.0" + "minimatch" "^3.0.4" + +"karma-jasmine-html-reporter@^1.5.0": + "integrity" "sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw==" + "resolved" "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz" + "version" "1.6.0" + +"karma-jasmine@>=1.1", "karma-jasmine@~4.0.0": + "integrity" "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==" + "resolved" "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "jasmine-core" "^3.6.0" + +"karma-source-map-support@1.4.0": + "integrity" "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==" + "resolved" "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "source-map-support" "^0.5.5" + +"karma@*", "karma@^6.3.0", "karma@>=0.9", "karma@~6.3.0": + "integrity" "sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==" + "resolved" "https://registry.npmjs.org/karma/-/karma-6.3.4.tgz" + "version" "6.3.4" + dependencies: + "body-parser" "^1.19.0" + "braces" "^3.0.2" + "chokidar" "^3.5.1" + "colors" "^1.4.0" + "connect" "^3.7.0" + "di" "^0.0.1" + "dom-serialize" "^2.2.1" + "glob" "^7.1.7" + "graceful-fs" "^4.2.6" + "http-proxy" "^1.18.1" + "isbinaryfile" "^4.0.8" + "lodash" "^4.17.21" + "log4js" "^6.3.0" + "mime" "^2.5.2" + "minimatch" "^3.0.4" + "qjobs" "^1.2.0" + "range-parser" "^1.2.1" + "rimraf" "^3.0.2" + "socket.io" "^3.1.0" + "source-map" "^0.6.1" + "tmp" "^0.2.1" + "ua-parser-js" "^0.7.28" + "yargs" "^16.1.1" + +"keyv@^3.0.0": + "integrity" "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==" + "resolved" "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "json-buffer" "3.0.0" + +"killable@^1.0.1": + "integrity" "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==" + "resolved" "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz" + "version" "1.0.1" + +"kind-of@^3.0.2": + "integrity" "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + "version" "3.2.2" + dependencies: + "is-buffer" "^1.1.5" + +"kind-of@^3.0.3": + "integrity" "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + "version" "3.2.2" + dependencies: + "is-buffer" "^1.1.5" + +"kind-of@^3.2.0": + "integrity" "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + "version" "3.2.2" + dependencies: + "is-buffer" "^1.1.5" + +"kind-of@^4.0.0": + "integrity" "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "is-buffer" "^1.1.5" + +"kind-of@^5.0.0": + "integrity" "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz" + "version" "5.1.0" + +"kind-of@^6.0.0", "kind-of@^6.0.2": + "integrity" "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "resolved" "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + "version" "6.0.3" + +"kleur@^3.0.3": + "integrity" "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + "resolved" "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + "version" "3.0.3" + +"klona@^2.0.4": + "integrity" "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + "resolved" "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz" + "version" "2.0.4" + +"latest-version@^5.1.0": + "integrity" "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==" + "resolved" "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz" + "version" "5.1.0" + dependencies: + "package-json" "^6.3.0" + +"less-loader@8.1.1": + "integrity" "sha512-K93jJU7fi3n6rxVvzp8Cb88Uy9tcQKfHlkoezHwKILXhlNYiRQl4yowLIkQqmBXOH/5I8yoKiYeIf781HGkW9g==" + "resolved" "https://registry.npmjs.org/less-loader/-/less-loader-8.1.1.tgz" + "version" "8.1.1" + dependencies: + "klona" "^2.0.4" + +"less@^3.5.0 || ^4.0.0", "less@4.1.1": + "integrity" "sha512-w09o8tZFPThBscl5d0Ggp3RcrKIouBoQscnOMgFH3n5V3kN/CXGHNfCkRPtxJk6nKryDXaV9aHLK55RXuH4sAw==" + "resolved" "https://registry.npmjs.org/less/-/less-4.1.1.tgz" + "version" "4.1.1" + dependencies: + "copy-anything" "^2.0.1" + "parse-node-version" "^1.0.1" + "tslib" "^1.10.0" + optionalDependencies: + "errno" "^0.1.1" + "graceful-fs" "^4.1.2" + "image-size" "~0.5.0" + "make-dir" "^2.1.0" + "mime" "^1.4.1" + "needle" "^2.5.2" + "source-map" "~0.6.0" + +"libnpmconfig@^1.2.1": + "integrity" "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==" + "resolved" "https://registry.npmjs.org/libnpmconfig/-/libnpmconfig-1.2.1.tgz" + "version" "1.2.1" + dependencies: + "figgy-pudding" "^3.5.1" + "find-up" "^3.0.0" + "ini" "^1.3.5" + +"license-webpack-plugin@2.3.19": + "integrity" "sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA==" + "resolved" "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz" + "version" "2.3.19" + dependencies: + "@types/webpack-sources" "^0.1.5" + "webpack-sources" "^1.2.0" + +"lines-and-columns@^1.1.6": + "integrity" "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "resolved" "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz" + "version" "1.1.6" + +"lit-element@^3.0.0-rc.2": + "integrity" "sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==" + "resolved" "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0-rc.2.tgz" + "version" "3.0.0-rc.2" + dependencies: + "@lit/reactive-element" "^1.0.0-rc.2" + "lit-html" "^2.0.0-rc.3" + +"lit-html@^2.0.0-rc.3": + "integrity" "sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==" + "resolved" "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0-rc.3.tgz" + "version" "2.0.0-rc.3" + dependencies: + "@types/trusted-types" "^1.0.1" + +"lit@2.0.0-rc.2": + "integrity" "sha512-BOCuoJR04WaTV8UqTKk09cNcQA10Aq2LCcBOiHuF7TzWH5RNDsbCBP5QM9sLBSotGTXbDug/gFO08jq6TbyEtw==" + "resolved" "https://registry.npmjs.org/lit/-/lit-2.0.0-rc.2.tgz" + "version" "2.0.0-rc.2" + dependencies: + "@lit/reactive-element" "^1.0.0-rc.2" + "lit-element" "^3.0.0-rc.2" + "lit-html" "^2.0.0-rc.3" + +"loader-runner@^4.2.0": + "integrity" "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" + "resolved" "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz" + "version" "4.2.0" + +"loader-utils@^1.4.0": + "integrity" "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==" + "resolved" "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "big.js" "^5.2.2" + "emojis-list" "^3.0.0" + "json5" "^1.0.1" + +"loader-utils@^2.0.0", "loader-utils@2.0.0": + "integrity" "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==" + "resolved" "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "big.js" "^5.2.2" + "emojis-list" "^3.0.0" + "json5" "^2.1.2" + +"locate-path@^3.0.0": + "integrity" "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==" + "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "p-locate" "^3.0.0" + "path-exists" "^3.0.0" + +"locate-path@^5.0.0": + "integrity" "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==" + "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "p-locate" "^4.1.0" + +"locate-path@^6.0.0": + "integrity" "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==" + "resolved" "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "p-locate" "^5.0.0" + +"lodash.assign@^4.2.0": + "integrity" "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + "resolved" "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz" + "version" "4.2.0" + +"lodash.debounce@^4.0.8": + "integrity" "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "resolved" "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + "version" "4.0.8" + +"lodash.memoize@^4.1.2": + "integrity" "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "resolved" "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + "version" "4.1.2" + +"lodash.uniq@^4.5.0": + "integrity" "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "resolved" "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" + "version" "4.5.0" + +"lodash@^4.17.11", "lodash@^4.17.13", "lodash@^4.17.14", "lodash@^4.17.15", "lodash@^4.17.21": + "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + "version" "4.17.21" + +"log-symbols@^4.1.0": + "integrity" "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==" + "resolved" "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "chalk" "^4.1.0" + "is-unicode-supported" "^0.1.0" + +"log4js@^6.3.0": + "integrity" "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==" + "resolved" "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz" + "version" "6.3.0" + dependencies: + "date-format" "^3.0.0" + "debug" "^4.1.1" + "flatted" "^2.0.1" + "rfdc" "^1.1.4" + "streamroller" "^2.2.4" + +"loglevel@^1.6.8": + "integrity" "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" + "resolved" "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz" + "version" "1.7.1" + +"lowercase-keys@^1.0.0", "lowercase-keys@^1.0.1": + "integrity" "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" + "version" "1.0.1" + +"lowercase-keys@^2.0.0": + "integrity" "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "resolved" "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" + "version" "2.0.0" + +"lru-cache@^6.0.0": + "integrity" "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==" + "resolved" "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "yallist" "^4.0.0" + +"magic-string@^0.25.0", "magic-string@0.25.7": + "integrity" "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==" + "resolved" "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" + "version" "0.25.7" + dependencies: + "sourcemap-codec" "^1.4.4" + +"make-dir@^2.1.0": + "integrity" "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==" + "resolved" "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "pify" "^4.0.1" + "semver" "^5.6.0" + +"make-dir@^3.0.0", "make-dir@^3.0.2", "make-dir@^3.1.0": + "integrity" "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==" + "resolved" "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "semver" "^6.0.0" + +"make-fetch-happen@^8.0.9": + "integrity" "sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==" + "resolved" "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz" + "version" "8.0.14" + dependencies: + "agentkeepalive" "^4.1.3" + "cacache" "^15.0.5" + "http-cache-semantics" "^4.1.0" + "http-proxy-agent" "^4.0.1" + "https-proxy-agent" "^5.0.0" + "is-lambda" "^1.0.1" + "lru-cache" "^6.0.0" + "minipass" "^3.1.3" + "minipass-collect" "^1.0.2" + "minipass-fetch" "^1.3.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "promise-retry" "^2.0.1" + "socks-proxy-agent" "^5.0.0" + "ssri" "^8.0.0" + +"make-fetch-happen@^9.0.1", "make-fetch-happen@^9.1.0": + "integrity" "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==" + "resolved" "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" + "version" "9.1.0" + dependencies: + "agentkeepalive" "^4.1.3" + "cacache" "^15.2.0" + "http-cache-semantics" "^4.1.0" + "http-proxy-agent" "^4.0.1" + "https-proxy-agent" "^5.0.0" + "is-lambda" "^1.0.1" + "lru-cache" "^6.0.0" + "minipass" "^3.1.3" + "minipass-collect" "^1.0.2" + "minipass-fetch" "^1.3.2" + "minipass-flush" "^1.0.5" + "minipass-pipeline" "^1.2.4" + "negotiator" "^0.6.2" + "promise-retry" "^2.0.1" + "socks-proxy-agent" "^6.0.0" + "ssri" "^8.0.0" + +"map-age-cleaner@^0.1.3": + "integrity" "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==" + "resolved" "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz" + "version" "0.1.3" + dependencies: + "p-defer" "^1.0.0" + +"map-cache@^0.2.2": + "integrity" "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "resolved" "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz" + "version" "0.2.2" + +"map-stream@0.0.7": + "integrity" "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" + "resolved" "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz" + "version" "0.0.7" + +"map-visit@^1.0.0": + "integrity" "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=" + "resolved" "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "object-visit" "^1.0.0" + +"mdn-data@2.0.14": + "integrity" "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "resolved" "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" + "version" "2.0.14" + +"media-typer@0.3.0": + "integrity" "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "resolved" "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + "version" "0.3.0" + +"mem@^8.0.0": + "integrity" "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==" + "resolved" "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz" + "version" "8.1.1" + dependencies: + "map-age-cleaner" "^0.1.3" + "mimic-fn" "^3.1.0" + +"memfs@^3.2.0": + "integrity" "sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==" + "resolved" "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz" + "version" "3.2.2" + dependencies: + "fs-monkey" "1.0.3" + +"memory-fs@^0.4.1": + "integrity" "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=" + "resolved" "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz" + "version" "0.4.1" + dependencies: + "errno" "^0.1.3" + "readable-stream" "^2.0.1" + +"merge-descriptors@1.0.1": + "integrity" "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "resolved" "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + "version" "1.0.1" + +"merge-source-map@^1.1.0": + "integrity" "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==" + "resolved" "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "source-map" "^0.6.1" + +"merge-stream@^2.0.0": + "integrity" "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "resolved" "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + "version" "2.0.0" + +"merge2@^1.3.0": + "integrity" "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "resolved" "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + "version" "1.4.1" + +"methods@~1.1.2": + "integrity" "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "resolved" "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + "version" "1.1.2" + +"micromatch@^3.1.10", "micromatch@^3.1.4": + "integrity" "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==" + "resolved" "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz" + "version" "3.1.10" + dependencies: + "arr-diff" "^4.0.0" + "array-unique" "^0.3.2" + "braces" "^2.3.1" + "define-property" "^2.0.2" + "extend-shallow" "^3.0.2" + "extglob" "^2.0.4" + "fragment-cache" "^0.2.1" + "kind-of" "^6.0.2" + "nanomatch" "^1.2.9" + "object.pick" "^1.3.0" + "regex-not" "^1.0.0" + "snapdragon" "^0.8.1" + "to-regex" "^3.0.2" + +"micromatch@^4.0.4": + "integrity" "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==" + "resolved" "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz" + "version" "4.0.4" + dependencies: + "braces" "^3.0.1" + "picomatch" "^2.2.3" + +"mime-db@>= 1.43.0 < 2", "mime-db@1.48.0": + "integrity" "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "resolved" "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz" + "version" "1.48.0" + +"mime-types@^2.1.12", "mime-types@^2.1.27", "mime-types@^2.1.28", "mime-types@~2.1.17", "mime-types@~2.1.19", "mime-types@~2.1.24": + "integrity" "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==" + "resolved" "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz" + "version" "2.1.31" + dependencies: + "mime-db" "1.48.0" + +"mime@^1.4.1", "mime@1.6.0": + "integrity" "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "resolved" "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + "version" "1.6.0" + +"mime@^2.4.4": + "integrity" "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + "resolved" "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz" + "version" "2.5.2" + +"mime@^2.5.2": + "integrity" "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + "resolved" "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz" + "version" "2.5.2" + +"mimic-fn@^2.1.0": + "integrity" "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "resolved" "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + "version" "2.1.0" + +"mimic-fn@^3.1.0": + "integrity" "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" + "resolved" "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz" + "version" "3.1.0" + +"mimic-response@^1.0.0", "mimic-response@^1.0.1": + "integrity" "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "resolved" "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" + "version" "1.0.1" + +"mingo@1": + "integrity" "sha1-aSLE0Ufvx3GgFCWixMj3eER4xUY=" + "resolved" "https://registry.npmjs.org/mingo/-/mingo-1.3.3.tgz" + "version" "1.3.3" + +"mini-css-extract-plugin@1.5.1": + "integrity" "sha512-wEpr0XooH6rw/Mlf+9KTJoMBLT3HujzdTrmohPjAzF47N4Q6yAeczQLpRD/WxvAtXvskcXbily7TAdCfi2M4Dg==" + "resolved" "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.5.1.tgz" + "version" "1.5.1" + dependencies: + "loader-utils" "^2.0.0" + "schema-utils" "^3.0.0" + "webpack-sources" "^1.1.0" + +"minimalistic-assert@^1.0.0": + "integrity" "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + "resolved" "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + "version" "1.0.1" + +"minimatch@^3.0.4", "minimatch@3.0.4": + "integrity" "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" + "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "brace-expansion" "^1.1.7" + +"minimist@^1.2.0", "minimist@^1.2.5": + "integrity" "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "resolved" "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" + "version" "1.2.5" + +"minipass-collect@^1.0.2": + "integrity" "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==" + "resolved" "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "minipass" "^3.0.0" + +"minipass-fetch@^1.3.0", "minipass-fetch@^1.3.2": + "integrity" "sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==" + "resolved" "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz" + "version" "1.3.3" + dependencies: + "minipass" "^3.1.0" + "minipass-sized" "^1.0.3" + "minizlib" "^2.0.0" + optionalDependencies: + "encoding" "^0.1.12" + +"minipass-flush@^1.0.5": + "integrity" "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==" + "resolved" "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" + "version" "1.0.5" + dependencies: + "minipass" "^3.0.0" + +"minipass-json-stream@^1.0.1": + "integrity" "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==" + "resolved" "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "jsonparse" "^1.3.1" + "minipass" "^3.0.0" + +"minipass-pipeline@^1.2.2", "minipass-pipeline@^1.2.4": + "integrity" "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==" + "resolved" "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + "version" "1.2.4" + dependencies: + "minipass" "^3.0.0" + +"minipass-sized@^1.0.3": + "integrity" "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==" + "resolved" "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" + "version" "1.0.3" + dependencies: + "minipass" "^3.0.0" + +"minipass@^3.0.0", "minipass@^3.1.0", "minipass@^3.1.1", "minipass@^3.1.3": + "integrity" "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==" + "resolved" "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz" + "version" "3.1.3" + dependencies: + "yallist" "^4.0.0" + +"minizlib@^2.0.0", "minizlib@^2.1.1": + "integrity" "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==" + "resolved" "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + "version" "2.1.2" + dependencies: + "minipass" "^3.0.0" + "yallist" "^4.0.0" + +"mixin-deep@^1.2.0": + "integrity" "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==" + "resolved" "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" + "version" "1.3.2" + dependencies: + "for-in" "^1.0.2" + "is-extendable" "^1.0.1" + +"mkdirp@^0.5.1": + "integrity" "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==" + "resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" + "version" "0.5.5" + dependencies: + "minimist" "^1.2.5" + +"mkdirp@^0.5.5": + "integrity" "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==" + "resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" + "version" "0.5.5" + dependencies: + "minimist" "^1.2.5" + +"mkdirp@^1.0.3", "mkdirp@^1.0.4", "mkdirp@~1.0.4": + "integrity" "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + "version" "1.0.4" + +"mockjs@^1.1.0": + "integrity" "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==" + "resolved" "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "commander" "*" + +"moment@^2.29.1": + "integrity" "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "resolved" "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" + "version" "2.29.1" + +"ms@^2.0.0", "ms@^2.1.1", "ms@2.1.2": + "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + "version" "2.1.2" + +"ms@2.0.0": + "integrity" "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "resolved" "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + "version" "2.0.0" + +"ms@2.1.1": + "integrity" "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" + "version" "2.1.1" + +"multicast-dns-service-types@^1.1.0": + "integrity" "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + "resolved" "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz" + "version" "1.1.0" + +"multicast-dns@^6.0.1": + "integrity" "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==" + "resolved" "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz" + "version" "6.2.3" + dependencies: + "dns-packet" "^1.3.1" + "thunky" "^1.0.2" + +"mute-stream@0.0.8": + "integrity" "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + "resolved" "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" + "version" "0.0.8" + +"nan@^2.12.1": + "integrity" "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "resolved" "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz" + "version" "2.14.2" + +"nanoid@^3.1.23": + "integrity" "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz" + "version" "3.1.23" + +"nanomatch@^1.2.9": + "integrity" "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==" + "resolved" "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz" + "version" "1.2.13" + dependencies: + "arr-diff" "^4.0.0" + "array-unique" "^0.3.2" + "define-property" "^2.0.2" + "extend-shallow" "^3.0.2" + "fragment-cache" "^0.2.1" + "is-windows" "^1.0.2" + "kind-of" "^6.0.2" + "object.pick" "^1.3.0" + "regex-not" "^1.0.0" + "snapdragon" "^0.8.1" + "to-regex" "^3.0.1" + +"needle@^2.5.2": + "integrity" "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==" + "resolved" "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz" + "version" "2.6.0" + dependencies: + "debug" "^3.2.6" + "iconv-lite" "^0.4.4" + "sax" "^1.2.4" + +"negotiator@^0.6.2", "negotiator@0.6.2": + "integrity" "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "resolved" "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" + "version" "0.6.2" + +"neo-async@^2.6.2": + "integrity" "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "resolved" "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + "version" "2.6.2" + +"nice-try@^1.0.4": + "integrity" "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "resolved" "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" + "version" "1.0.5" + +"node-forge@^0.10.0": + "integrity" "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + "resolved" "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz" + "version" "0.10.0" + +"node-gyp@^7.1.0": + "integrity" "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==" + "resolved" "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz" + "version" "7.1.2" + dependencies: + "env-paths" "^2.2.0" + "glob" "^7.1.4" + "graceful-fs" "^4.2.3" + "nopt" "^5.0.0" + "npmlog" "^4.1.2" + "request" "^2.88.2" + "rimraf" "^3.0.2" + "semver" "^7.3.2" + "tar" "^6.0.2" + "which" "^2.0.2" + +"node-gyp@^8.2.0": + "integrity" "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==" + "resolved" "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" + "version" "8.4.1" + dependencies: + "env-paths" "^2.2.0" + "glob" "^7.1.4" + "graceful-fs" "^4.2.6" + "make-fetch-happen" "^9.1.0" + "nopt" "^5.0.0" + "npmlog" "^6.0.0" + "rimraf" "^3.0.2" + "semver" "^7.3.5" + "tar" "^6.1.2" + "which" "^2.0.2" + +"node-releases@^1.1.71": + "integrity" "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" + "resolved" "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz" + "version" "1.1.73" + +"nopt@^5.0.0": + "integrity" "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==" + "resolved" "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "abbrev" "1" + +"normalize-path@^2.1.1": + "integrity" "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=" + "resolved" "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "remove-trailing-separator" "^1.0.1" + +"normalize-path@^3.0.0", "normalize-path@~3.0.0": + "integrity" "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "resolved" "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + "version" "3.0.0" + +"normalize-range@^0.1.2": + "integrity" "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "resolved" "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + "version" "0.1.2" + +"normalize-url@^4.1.0": + "integrity" "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + "resolved" "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" + "version" "4.5.1" + +"normalize-url@^6.0.1": + "integrity" "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + "resolved" "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" + "version" "6.1.0" + +"normalize.css@^8.0.1": + "integrity" "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" + "resolved" "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz" + "version" "8.0.1" + +"npm-bundled@^1.1.1": + "integrity" "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==" + "resolved" "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz" + "version" "1.1.2" + dependencies: + "npm-normalize-package-bin" "^1.0.1" + +"npm-check-updates@^12.2.0": + "integrity" "sha512-upiHqivxtjtj/iHO7Amnd+Q5Nx5HzTwxMlu9lUzNFQvCAAFhJnPK8Weuoyz3OSwErpHVxJMry7W5d0H7Cokxdg==" + "resolved" "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-12.2.0.tgz" + "version" "12.2.0" + dependencies: + "chalk" "^4.1.2" + "cint" "^8.2.1" + "cli-table" "^0.3.11" + "commander" "^8.3.0" + "fast-memoize" "^2.5.2" + "find-up" "5.0.0" + "fp-and-or" "^0.1.3" + "get-stdin" "^8.0.0" + "globby" "^11.0.4" + "hosted-git-info" "^4.0.2" + "json-parse-helpfulerror" "^1.0.3" + "jsonlines" "^0.1.1" + "libnpmconfig" "^1.2.1" + "lodash" "^4.17.21" + "minimatch" "^3.0.4" + "p-map" "^4.0.0" + "pacote" "^12.0.2" + "parse-github-url" "^1.0.2" + "progress" "^2.0.3" + "prompts" "^2.4.2" + "rc-config-loader" "^4.0.0" + "remote-git-tags" "^3.0.0" + "rimraf" "^3.0.2" + "semver" "^7.3.5" + "semver-utils" "^1.1.4" + "source-map-support" "^0.5.21" + "spawn-please" "^1.0.0" + "update-notifier" "^5.1.0" + +"npm-install-checks@^4.0.0": + "integrity" "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==" + "resolved" "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "semver" "^7.1.1" + +"npm-normalize-package-bin@^1.0.1": + "integrity" "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + "resolved" "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz" + "version" "1.0.1" + +"npm-package-arg@^8.0.0", "npm-package-arg@^8.0.1", "npm-package-arg@^8.1.2", "npm-package-arg@8.1.2": + "integrity" "sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA==" + "resolved" "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.2.tgz" + "version" "8.1.2" + dependencies: + "hosted-git-info" "^4.0.1" + "semver" "^7.3.4" + "validate-npm-package-name" "^3.0.0" + +"npm-packlist@^2.1.4": + "integrity" "sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==" + "resolved" "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz" + "version" "2.2.2" + dependencies: + "glob" "^7.1.6" + "ignore-walk" "^3.0.3" + "npm-bundled" "^1.1.1" + "npm-normalize-package-bin" "^1.0.1" + +"npm-packlist@^3.0.0": + "integrity" "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==" + "resolved" "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "glob" "^7.1.6" + "ignore-walk" "^4.0.1" + "npm-bundled" "^1.1.1" + "npm-normalize-package-bin" "^1.0.1" + +"npm-pick-manifest@^6.0.0", "npm-pick-manifest@^6.1.1", "npm-pick-manifest@6.1.1": + "integrity" "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==" + "resolved" "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz" + "version" "6.1.1" + dependencies: + "npm-install-checks" "^4.0.0" + "npm-normalize-package-bin" "^1.0.1" + "npm-package-arg" "^8.1.2" + "semver" "^7.3.4" + +"npm-registry-fetch@^10.0.0": + "integrity" "sha512-KsM/TdPmntqgBFlfsbkOLkkE9ovZo7VpVcd+/eTdYszCrgy5zFl5JzWm+OxavFaEWlbkirpkou+ZYI00RmOBFA==" + "resolved" "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-10.1.2.tgz" + "version" "10.1.2" + dependencies: + "lru-cache" "^6.0.0" + "make-fetch-happen" "^8.0.9" + "minipass" "^3.1.3" + "minipass-fetch" "^1.3.0" + "minipass-json-stream" "^1.0.1" + "minizlib" "^2.0.0" + "npm-package-arg" "^8.0.0" + +"npm-registry-fetch@^11.0.0": + "integrity" "sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA==" + "resolved" "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz" + "version" "11.0.0" + dependencies: + "make-fetch-happen" "^9.0.1" + "minipass" "^3.1.3" + "minipass-fetch" "^1.3.0" + "minipass-json-stream" "^1.0.1" + "minizlib" "^2.0.0" + "npm-package-arg" "^8.0.0" + +"npm-run-path@^2.0.0": + "integrity" "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=" + "resolved" "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "path-key" "^2.0.0" + +"npmlog@^4.1.2": + "integrity" "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==" + "resolved" "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "are-we-there-yet" "~1.1.2" + "console-control-strings" "~1.1.0" + "gauge" "~2.7.3" + "set-blocking" "~2.0.0" + +"npmlog@^6.0.0": + "integrity" "sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q==" + "resolved" "https://registry.npmjs.org/npmlog/-/npmlog-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "are-we-there-yet" "^2.0.0" + "console-control-strings" "^1.1.0" + "gauge" "^4.0.0" + "set-blocking" "^2.0.0" + +"nth-check@^2.0.0": + "integrity" "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==" + "resolved" "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "boolbase" "^1.0.0" + +"num2fraction@^1.2.2": + "integrity" "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + "resolved" "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz" + "version" "1.2.2" + +"number-is-nan@^1.0.0": + "integrity" "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "resolved" "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + "version" "1.0.1" + +"oauth-sign@~0.9.0": + "integrity" "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "resolved" "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + "version" "0.9.0" + +"object-assign@^4", "object-assign@^4.0.1", "object-assign@^4.1.0": + "integrity" "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "resolved" "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + "version" "4.1.1" + +"object-copy@^0.1.0": + "integrity" "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=" + "resolved" "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz" + "version" "0.1.0" + dependencies: + "copy-descriptor" "^0.1.0" + "define-property" "^0.2.5" + "kind-of" "^3.0.3" + +"object-is@^1.0.1": + "integrity" "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==" + "resolved" "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" + "version" "1.1.5" + dependencies: + "call-bind" "^1.0.2" + "define-properties" "^1.1.3" + +"object-keys@^1.0.12", "object-keys@^1.1.1": + "integrity" "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "resolved" "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + "version" "1.1.1" + +"object-visit@^1.0.0": + "integrity" "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=" + "resolved" "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "isobject" "^3.0.0" + +"object.assign@^4.1.0": + "integrity" "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==" + "resolved" "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "call-bind" "^1.0.0" + "define-properties" "^1.1.3" + "has-symbols" "^1.0.1" + "object-keys" "^1.1.1" + +"object.pick@^1.3.0": + "integrity" "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=" + "resolved" "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz" + "version" "1.3.0" + dependencies: + "isobject" "^3.0.1" + +"obuf@^1.0.0", "obuf@^1.1.2": + "integrity" "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + "resolved" "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + "version" "1.1.2" + +"on-finished@~2.3.0": + "integrity" "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + "resolved" "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "ee-first" "1.1.1" + +"on-headers@~1.0.2": + "integrity" "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + "resolved" "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + "version" "1.0.2" + +"once@^1.3.0", "once@^1.3.1", "once@^1.4.0": + "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "wrappy" "1" + +"onetime@^5.1.0": + "integrity" "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==" + "resolved" "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "mimic-fn" "^2.1.0" + +"open@8.0.2": + "integrity" "sha512-NV5QmWJrTaNBLHABJyrb+nd5dXI5zfea/suWawBhkHzAbVhLLiJdrqMgxMypGK9Eznp2Ltoh7SAVkQ3XAucX7Q==" + "resolved" "https://registry.npmjs.org/open/-/open-8.0.2.tgz" + "version" "8.0.2" + dependencies: + "define-lazy-prop" "^2.0.0" + "is-docker" "^2.1.1" + "is-wsl" "^2.2.0" + +"opn@^5.5.0": + "integrity" "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==" + "resolved" "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz" + "version" "5.5.0" + dependencies: + "is-wsl" "^1.1.0" + +"ora@5.4.0": + "integrity" "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==" + "resolved" "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz" + "version" "5.4.0" + dependencies: + "bl" "^4.1.0" + "chalk" "^4.1.0" + "cli-cursor" "^3.1.0" + "cli-spinners" "^2.5.0" + "is-interactive" "^1.0.0" + "is-unicode-supported" "^0.1.0" + "log-symbols" "^4.1.0" + "strip-ansi" "^6.0.0" + "wcwidth" "^1.0.1" + +"original@^1.0.0": + "integrity" "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==" + "resolved" "https://registry.npmjs.org/original/-/original-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "url-parse" "^1.4.3" + +"os-tmpdir@~1.0.2": + "integrity" "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "resolved" "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + "version" "1.0.2" + +"p-cancelable@^1.0.0": + "integrity" "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + "resolved" "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" + "version" "1.1.0" + +"p-defer@^1.0.0": + "integrity" "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + "resolved" "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz" + "version" "1.0.0" + +"p-finally@^1.0.0": + "integrity" "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + "resolved" "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" + "version" "1.0.0" + +"p-limit@^2.0.0", "p-limit@^2.2.0": + "integrity" "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==" + "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "p-try" "^2.0.0" + +"p-limit@^3.0.2": + "integrity" "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==" + "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "yocto-queue" "^0.1.0" + +"p-limit@^3.1.0": + "integrity" "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==" + "resolved" "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "yocto-queue" "^0.1.0" + +"p-locate@^3.0.0": + "integrity" "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==" + "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "p-limit" "^2.0.0" + +"p-locate@^4.1.0": + "integrity" "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==" + "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "p-limit" "^2.2.0" + +"p-locate@^5.0.0": + "integrity" "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==" + "resolved" "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "p-limit" "^3.0.2" + +"p-map@^2.0.0": + "integrity" "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + "resolved" "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" + "version" "2.1.0" + +"p-map@^4.0.0": + "integrity" "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==" + "resolved" "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "aggregate-error" "^3.0.0" + +"p-retry@^3.0.1": + "integrity" "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==" + "resolved" "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "retry" "^0.12.0" + +"p-try@^2.0.0": + "integrity" "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "resolved" "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + "version" "2.2.0" + +"package-json@^6.3.0": + "integrity" "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==" + "resolved" "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz" + "version" "6.5.0" + dependencies: + "got" "^9.6.0" + "registry-auth-token" "^4.0.0" + "registry-url" "^5.0.0" + "semver" "^6.2.0" + +"pacote@^12.0.2": + "integrity" "sha512-Ar3mhjcxhMzk+OVZ8pbnXdb0l8+pimvlsqBGRNkble2NVgyqOGE3yrCGi/lAYq7E7NRDMz89R1Wx5HIMCGgeYg==" + "resolved" "https://registry.npmjs.org/pacote/-/pacote-12.0.2.tgz" + "version" "12.0.2" + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^2.0.0" + "cacache" "^15.0.5" + "chownr" "^2.0.0" + "fs-minipass" "^2.1.0" + "infer-owner" "^1.0.4" + "minipass" "^3.1.3" + "mkdirp" "^1.0.3" + "npm-package-arg" "^8.0.1" + "npm-packlist" "^3.0.0" + "npm-pick-manifest" "^6.0.0" + "npm-registry-fetch" "^11.0.0" + "promise-retry" "^2.0.1" + "read-package-json-fast" "^2.0.1" + "rimraf" "^3.0.2" + "ssri" "^8.0.1" + "tar" "^6.1.0" + +"pacote@11.3.2": + "integrity" "sha512-lMO7V9aMhyE5gfaSFxKfW3OTdXuFBNQJfuNuet3NPzWWhOYIW90t85vHcHLDjdhgmfAdAHyh9q1HAap96ea0XA==" + "resolved" "https://registry.npmjs.org/pacote/-/pacote-11.3.2.tgz" + "version" "11.3.2" + dependencies: + "@npmcli/git" "^2.0.1" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + "cacache" "^15.0.5" + "chownr" "^2.0.0" + "fs-minipass" "^2.1.0" + "infer-owner" "^1.0.4" + "minipass" "^3.1.3" + "mkdirp" "^1.0.3" + "npm-package-arg" "^8.0.1" + "npm-packlist" "^2.1.4" + "npm-pick-manifest" "^6.0.0" + "npm-registry-fetch" "^10.0.0" + "promise-retry" "^2.0.1" + "read-package-json-fast" "^2.0.1" + "rimraf" "^3.0.2" + "ssri" "^8.0.1" + "tar" "^6.1.0" + +"parent-module@^1.0.0": + "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" + "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "callsites" "^3.0.0" + +"parse-github-url@^1.0.2": + "integrity" "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==" + "resolved" "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz" + "version" "1.0.2" + +"parse-json@^5.0.0": + "integrity" "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==" + "resolved" "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "@babel/code-frame" "^7.0.0" + "error-ex" "^1.3.1" + "json-parse-even-better-errors" "^2.3.0" + "lines-and-columns" "^1.1.6" + +"parse-node-version@^1.0.1": + "integrity" "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==" + "resolved" "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz" + "version" "1.0.1" + +"parse5-html-rewriting-stream@6.0.1": + "integrity" "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==" + "resolved" "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "parse5" "^6.0.1" + "parse5-sax-parser" "^6.0.1" + +"parse5-htmlparser2-tree-adapter@^6.0.1": + "integrity" "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==" + "resolved" "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "parse5" "^6.0.1" + +"parse5-sax-parser@^6.0.1": + "integrity" "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==" + "resolved" "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "parse5" "^6.0.1" + +"parse5@^5.0.0": + "integrity" "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + "resolved" "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz" + "version" "5.1.1" + +"parse5@^6.0.1": + "integrity" "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + "resolved" "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" + "version" "6.0.1" + +"parseurl@~1.3.2", "parseurl@~1.3.3": + "integrity" "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "resolved" "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + "version" "1.3.3" + +"pascalcase@^0.1.1": + "integrity" "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "resolved" "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz" + "version" "0.1.1" + +"path-dirname@^1.0.0": + "integrity" "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + "resolved" "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz" + "version" "1.0.2" + +"path-exists@^3.0.0": + "integrity" "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "resolved" "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" + "version" "3.0.0" + +"path-exists@^4.0.0": + "integrity" "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "resolved" "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + "version" "4.0.0" + +"path-is-absolute@^1.0.0": + "integrity" "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "version" "1.0.1" + +"path-is-inside@^1.0.2": + "integrity" "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + "resolved" "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" + "version" "1.0.2" + +"path-key@^2.0.0", "path-key@^2.0.1": + "integrity" "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "resolved" "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" + "version" "2.0.1" + +"path-parse@^1.0.6": + "integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "resolved" "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + "version" "1.0.7" + +"path-to-regexp@0.1.7": + "integrity" "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "resolved" "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + "version" "0.1.7" + +"path-type@^4.0.0": + "integrity" "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "resolved" "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + "version" "4.0.0" + +"pause-stream@^0.0.11": + "integrity" "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=" + "resolved" "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz" + "version" "0.0.11" + dependencies: + "through" "~2.3" + +"performance-now@^2.1.0": + "integrity" "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "resolved" "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + "version" "2.1.0" + +"picomatch@^2.0.4", "picomatch@^2.2.1", "picomatch@^2.2.3": + "integrity" "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + "resolved" "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" + "version" "2.3.0" + +"pify@^2.0.0": + "integrity" "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "resolved" "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + "version" "2.3.0" + +"pify@^2.3.0": + "integrity" "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "resolved" "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + "version" "2.3.0" + +"pify@^4.0.1": + "integrity" "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + "resolved" "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + "version" "4.0.1" + +"pinkie-promise@^2.0.0": + "integrity" "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" + "resolved" "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "pinkie" "^2.0.0" + +"pinkie@^2.0.0": + "integrity" "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "resolved" "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + "version" "2.0.4" + +"pkg-dir@^3.0.0": + "integrity" "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==" + "resolved" "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "find-up" "^3.0.0" + +"pkg-dir@^4.1.0": + "integrity" "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==" + "resolved" "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + "version" "4.2.0" + dependencies: + "find-up" "^4.0.0" + +"portfinder@^1.0.26": + "integrity" "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==" + "resolved" "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz" + "version" "1.0.28" + dependencies: + "async" "^2.6.2" + "debug" "^3.1.1" + "mkdirp" "^0.5.5" + +"posix-character-classes@^0.1.0": + "integrity" "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "resolved" "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz" + "version" "0.1.1" + +"postcss-attribute-case-insensitive@^4.0.1": + "integrity" "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==" + "resolved" "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "postcss" "^7.0.2" + "postcss-selector-parser" "^6.0.2" + +"postcss-calc@^8.0.0": + "integrity" "sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==" + "resolved" "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz" + "version" "8.0.0" + dependencies: + "postcss-selector-parser" "^6.0.2" + "postcss-value-parser" "^4.0.2" + +"postcss-color-functional-notation@^2.0.1": + "integrity" "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==" + "resolved" "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-color-gray@^5.0.0": + "integrity" "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==" + "resolved" "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "@csstools/convert-colors" "^1.4.0" + "postcss" "^7.0.5" + "postcss-values-parser" "^2.0.0" + +"postcss-color-hex-alpha@^5.0.3": + "integrity" "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==" + "resolved" "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz" + "version" "5.0.3" + dependencies: + "postcss" "^7.0.14" + "postcss-values-parser" "^2.0.1" + +"postcss-color-mod-function@^3.0.3": + "integrity" "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==" + "resolved" "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz" + "version" "3.0.3" + dependencies: + "@csstools/convert-colors" "^1.4.0" + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-color-rebeccapurple@^4.0.1": + "integrity" "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==" + "resolved" "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-colormin@^5.2.0": + "integrity" "sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==" + "resolved" "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "browserslist" "^4.16.6" + "caniuse-api" "^3.0.0" + "colord" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-convert-values@^5.0.1": + "integrity" "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==" + "resolved" "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "postcss-value-parser" "^4.1.0" + +"postcss-custom-media@^7.0.8": + "integrity" "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==" + "resolved" "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz" + "version" "7.0.8" + dependencies: + "postcss" "^7.0.14" + +"postcss-custom-properties@^8.0.11": + "integrity" "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==" + "resolved" "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz" + "version" "8.0.11" + dependencies: + "postcss" "^7.0.17" + "postcss-values-parser" "^2.0.1" + +"postcss-custom-selectors@^5.1.2": + "integrity" "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==" + "resolved" "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "postcss" "^7.0.2" + "postcss-selector-parser" "^5.0.0-rc.3" + +"postcss-dir-pseudo-class@^5.0.0": + "integrity" "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==" + "resolved" "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "postcss" "^7.0.2" + "postcss-selector-parser" "^5.0.0-rc.3" + +"postcss-discard-comments@^5.0.1": + "integrity" "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" + "resolved" "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz" + "version" "5.0.1" + +"postcss-discard-duplicates@^5.0.1": + "integrity" "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" + "resolved" "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz" + "version" "5.0.1" + +"postcss-discard-empty@^5.0.1": + "integrity" "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" + "resolved" "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz" + "version" "5.0.1" + +"postcss-discard-overridden@^5.0.1": + "integrity" "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" + "resolved" "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz" + "version" "5.0.1" + +"postcss-double-position-gradients@^1.0.0": + "integrity" "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==" + "resolved" "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "postcss" "^7.0.5" + "postcss-values-parser" "^2.0.0" + +"postcss-env-function@^2.0.2": + "integrity" "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==" + "resolved" "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-focus-visible@^4.0.0": + "integrity" "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==" + "resolved" "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-focus-within@^3.0.0": + "integrity" "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==" + "resolved" "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-font-variant@^4.0.0": + "integrity" "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==" + "resolved" "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "postcss" "^7.0.2" + +"postcss-gap-properties@^2.0.0": + "integrity" "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==" + "resolved" "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-image-set-function@^3.0.1": + "integrity" "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==" + "resolved" "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-import@14.0.1": + "integrity" "sha512-Xn2+z++vWObbEPhiiKO1a78JiyhqipyrXHBb3AHpv0ks7Cdg+GxQQJ24ODNMTanldf7197gSP3axppO9yaG0lA==" + "resolved" "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.1.tgz" + "version" "14.0.1" + dependencies: + "postcss-value-parser" "^4.0.0" + "read-cache" "^1.0.0" + "resolve" "^1.1.7" + +"postcss-initial@^3.0.0": + "integrity" "sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg==" + "resolved" "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "postcss" "^7.0.2" + +"postcss-lab-function@^2.0.1": + "integrity" "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==" + "resolved" "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "@csstools/convert-colors" "^1.4.0" + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-loader@5.2.0": + "integrity" "sha512-uSuCkENFeUaOYsKrXm0eNNgVIxc71z8RcckLMbVw473rGojFnrUeqEz6zBgXsH2q1EIzXnO/4pEz9RhALjlITA==" + "resolved" "https://registry.npmjs.org/postcss-loader/-/postcss-loader-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "cosmiconfig" "^7.0.0" + "klona" "^2.0.4" + "semver" "^7.3.4" + +"postcss-logical@^3.0.0": + "integrity" "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==" + "resolved" "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-media-minmax@^4.0.0": + "integrity" "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==" + "resolved" "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-merge-longhand@^5.0.2": + "integrity" "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==" + "resolved" "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz" + "version" "5.0.2" + dependencies: + "css-color-names" "^1.0.1" + "postcss-value-parser" "^4.1.0" + "stylehacks" "^5.0.1" + +"postcss-merge-rules@^5.0.2": + "integrity" "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==" + "resolved" "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz" + "version" "5.0.2" + dependencies: + "browserslist" "^4.16.6" + "caniuse-api" "^3.0.0" + "cssnano-utils" "^2.0.1" + "postcss-selector-parser" "^6.0.5" + "vendors" "^1.0.3" + +"postcss-minify-font-values@^5.0.1": + "integrity" "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==" + "resolved" "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "postcss-value-parser" "^4.1.0" + +"postcss-minify-gradients@^5.0.1": + "integrity" "sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g==" + "resolved" "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "cssnano-utils" "^2.0.1" + "is-color-stop" "^1.1.0" + "postcss-value-parser" "^4.1.0" + +"postcss-minify-params@^5.0.1": + "integrity" "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==" + "resolved" "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "alphanum-sort" "^1.0.2" + "browserslist" "^4.16.0" + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + "uniqs" "^2.0.0" + +"postcss-minify-selectors@^5.1.0": + "integrity" "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==" + "resolved" "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz" + "version" "5.1.0" + dependencies: + "alphanum-sort" "^1.0.2" + "postcss-selector-parser" "^6.0.5" + +"postcss-modules-extract-imports@^3.0.0": + "integrity" "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" + "resolved" "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" + "version" "3.0.0" + +"postcss-modules-local-by-default@^4.0.0": + "integrity" "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==" + "resolved" "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "icss-utils" "^5.0.0" + "postcss-selector-parser" "^6.0.2" + "postcss-value-parser" "^4.1.0" + +"postcss-modules-scope@^3.0.0": + "integrity" "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==" + "resolved" "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "postcss-selector-parser" "^6.0.4" + +"postcss-modules-values@^4.0.0": + "integrity" "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==" + "resolved" "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "icss-utils" "^5.0.0" + +"postcss-nesting@^7.0.0": + "integrity" "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==" + "resolved" "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz" + "version" "7.0.1" + dependencies: + "postcss" "^7.0.2" + +"postcss-normalize-charset@^5.0.1": + "integrity" "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" + "resolved" "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz" + "version" "5.0.1" + +"postcss-normalize-display-values@^5.0.1": + "integrity" "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==" + "resolved" "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-positions@^5.0.1": + "integrity" "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==" + "resolved" "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-repeat-style@^5.0.1": + "integrity" "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==" + "resolved" "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-string@^5.0.1": + "integrity" "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==" + "resolved" "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-timing-functions@^5.0.1": + "integrity" "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==" + "resolved" "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-unicode@^5.0.1": + "integrity" "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==" + "resolved" "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "browserslist" "^4.16.0" + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-url@^5.0.2": + "integrity" "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==" + "resolved" "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz" + "version" "5.0.2" + dependencies: + "is-absolute-url" "^3.0.3" + "normalize-url" "^6.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-normalize-whitespace@^5.0.1": + "integrity" "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==" + "resolved" "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "postcss-value-parser" "^4.1.0" + +"postcss-ordered-values@^5.0.2": + "integrity" "sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==" + "resolved" "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz" + "version" "5.0.2" + dependencies: + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-overflow-shorthand@^2.0.0": + "integrity" "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==" + "resolved" "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-page-break@^2.0.0": + "integrity" "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==" + "resolved" "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-place@^4.0.1": + "integrity" "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==" + "resolved" "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "postcss" "^7.0.2" + "postcss-values-parser" "^2.0.0" + +"postcss-preset-env@6.7.0": + "integrity" "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==" + "resolved" "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz" + "version" "6.7.0" + dependencies: + "autoprefixer" "^9.6.1" + "browserslist" "^4.6.4" + "caniuse-lite" "^1.0.30000981" + "css-blank-pseudo" "^0.1.4" + "css-has-pseudo" "^0.10.0" + "css-prefers-color-scheme" "^3.1.1" + "cssdb" "^4.4.0" + "postcss" "^7.0.17" + "postcss-attribute-case-insensitive" "^4.0.1" + "postcss-color-functional-notation" "^2.0.1" + "postcss-color-gray" "^5.0.0" + "postcss-color-hex-alpha" "^5.0.3" + "postcss-color-mod-function" "^3.0.3" + "postcss-color-rebeccapurple" "^4.0.1" + "postcss-custom-media" "^7.0.8" + "postcss-custom-properties" "^8.0.11" + "postcss-custom-selectors" "^5.1.2" + "postcss-dir-pseudo-class" "^5.0.0" + "postcss-double-position-gradients" "^1.0.0" + "postcss-env-function" "^2.0.2" + "postcss-focus-visible" "^4.0.0" + "postcss-focus-within" "^3.0.0" + "postcss-font-variant" "^4.0.0" + "postcss-gap-properties" "^2.0.0" + "postcss-image-set-function" "^3.0.1" + "postcss-initial" "^3.0.0" + "postcss-lab-function" "^2.0.1" + "postcss-logical" "^3.0.0" + "postcss-media-minmax" "^4.0.0" + "postcss-nesting" "^7.0.0" + "postcss-overflow-shorthand" "^2.0.0" + "postcss-page-break" "^2.0.0" + "postcss-place" "^4.0.1" + "postcss-pseudo-class-any-link" "^6.0.0" + "postcss-replace-overflow-wrap" "^3.0.0" + "postcss-selector-matches" "^4.0.0" + "postcss-selector-not" "^4.0.0" + +"postcss-pseudo-class-any-link@^6.0.0": + "integrity" "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==" + "resolved" "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "postcss" "^7.0.2" + "postcss-selector-parser" "^5.0.0-rc.3" + +"postcss-reduce-initial@^5.0.1": + "integrity" "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==" + "resolved" "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "browserslist" "^4.16.0" + "caniuse-api" "^3.0.0" + +"postcss-reduce-transforms@^5.0.1": + "integrity" "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==" + "resolved" "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "cssnano-utils" "^2.0.1" + "postcss-value-parser" "^4.1.0" + +"postcss-replace-overflow-wrap@^3.0.0": + "integrity" "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==" + "resolved" "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "postcss" "^7.0.2" + +"postcss-selector-matches@^4.0.0": + "integrity" "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==" + "resolved" "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "balanced-match" "^1.0.0" + "postcss" "^7.0.2" + +"postcss-selector-not@^4.0.0": + "integrity" "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==" + "resolved" "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz" + "version" "4.0.1" + dependencies: + "balanced-match" "^1.0.0" + "postcss" "^7.0.2" + +"postcss-selector-parser@^5.0.0-rc.3": + "integrity" "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==" + "resolved" "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "cssesc" "^2.0.0" + "indexes-of" "^1.0.1" + "uniq" "^1.0.1" + +"postcss-selector-parser@^5.0.0-rc.4": + "integrity" "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==" + "resolved" "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "cssesc" "^2.0.0" + "indexes-of" "^1.0.1" + "uniq" "^1.0.1" + +"postcss-selector-parser@^6.0.2", "postcss-selector-parser@^6.0.4", "postcss-selector-parser@^6.0.5": + "integrity" "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==" + "resolved" "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz" + "version" "6.0.6" + dependencies: + "cssesc" "^3.0.0" + "util-deprecate" "^1.0.2" + +"postcss-svgo@^5.0.2": + "integrity" "sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==" + "resolved" "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.2.tgz" + "version" "5.0.2" + dependencies: + "postcss-value-parser" "^4.1.0" + "svgo" "^2.3.0" + +"postcss-unique-selectors@^5.0.1": + "integrity" "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==" + "resolved" "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "alphanum-sort" "^1.0.2" + "postcss-selector-parser" "^6.0.5" + "uniqs" "^2.0.0" + +"postcss-value-parser@^4.0.0", "postcss-value-parser@^4.0.2", "postcss-value-parser@^4.1.0": + "integrity" "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "resolved" "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz" + "version" "4.1.0" + +"postcss-values-parser@^2.0.0", "postcss-values-parser@^2.0.1": + "integrity" "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==" + "resolved" "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "flatten" "^1.0.2" + "indexes-of" "^1.0.1" + "uniq" "^1.0.1" + +"postcss@^7.0.0 || ^8.0.1", "postcss@^8.0.0", "postcss@^8.0.9", "postcss@^8.1.0", "postcss@^8.2.10", "postcss@^8.2.15", "postcss@^8.2.2", "postcss@^8.2.9", "postcss@8.3.0": + "integrity" "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz" + "version" "8.3.0" + dependencies: + "colorette" "^1.2.2" + "nanoid" "^3.1.23" + "source-map-js" "^0.6.2" + +"postcss@^7.0.14": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.17": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.2": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.32": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.35": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.5": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"postcss@^7.0.6": + "integrity" "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==" + "resolved" "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz" + "version" "7.0.36" + dependencies: + "chalk" "^2.4.2" + "source-map" "^0.6.1" + "supports-color" "^6.1.0" + +"prepend-http@^2.0.0": + "integrity" "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + "resolved" "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" + "version" "2.0.0" + +"pretty-bytes@^5.3.0": + "integrity" "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" + "resolved" "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" + "version" "5.6.0" + +"process-nextick-args@~2.0.0": + "integrity" "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "resolved" "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + "version" "2.0.1" + +"progress@^2.0.3": + "integrity" "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + "resolved" "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" + "version" "2.0.3" + +"promise-inflight@^1.0.1": + "integrity" "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "resolved" "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" + "version" "1.0.1" + +"promise-retry@^2.0.1": + "integrity" "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==" + "resolved" "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "err-code" "^2.0.2" + "retry" "^0.12.0" + +"prompts@^2.4.2": + "integrity" "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==" + "resolved" "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + "version" "2.4.2" + dependencies: + "kleur" "^3.0.3" + "sisteransi" "^1.0.5" + +"proxy-addr@~2.0.5": + "integrity" "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==" + "resolved" "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + "version" "2.0.7" + dependencies: + "forwarded" "0.2.0" + "ipaddr.js" "1.9.1" + +"prr@~1.0.1": + "integrity" "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + "resolved" "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" + "version" "1.0.1" + +"psl@^1.1.28": + "integrity" "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + "resolved" "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" + "version" "1.8.0" + +"pump@^3.0.0": + "integrity" "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==" + "resolved" "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "end-of-stream" "^1.1.0" + "once" "^1.3.1" + +"punycode@^2.1.0", "punycode@^2.1.1": + "integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + "version" "2.1.1" + +"punycode@1.3.2": + "integrity" "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + "resolved" "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" + "version" "1.3.2" + +"pupa@^2.1.1": + "integrity" "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==" + "resolved" "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "escape-goat" "^2.0.0" + +"qjobs@^1.2.0": + "integrity" "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==" + "resolved" "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz" + "version" "1.2.0" + +"qs@~6.5.2": + "integrity" "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "resolved" "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + "version" "6.5.2" + +"qs@6.7.0": + "integrity" "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "resolved" "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz" + "version" "6.7.0" + +"querystring@0.2.0": + "integrity" "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "resolved" "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" + "version" "0.2.0" + +"querystringify@^2.1.1": + "integrity" "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "resolved" "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + "version" "2.2.0" + +"queue-microtask@^1.2.2": + "integrity" "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + "version" "1.2.3" + +"ramda@^0.27.1": + "integrity" "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" + "resolved" "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz" + "version" "0.27.1" + +"randombytes@^2.1.0": + "integrity" "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==" + "resolved" "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + "version" "2.1.0" + dependencies: + "safe-buffer" "^5.1.0" + +"range-parser@^1.2.1", "range-parser@~1.2.1": + "integrity" "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "resolved" "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + "version" "1.2.1" + +"raw-body@2.4.0": + "integrity" "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==" + "resolved" "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz" + "version" "2.4.0" + dependencies: + "bytes" "3.1.0" + "http-errors" "1.7.2" + "iconv-lite" "0.4.24" + "unpipe" "1.0.0" + +"raw-loader@4.0.2": + "integrity" "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==" + "resolved" "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "loader-utils" "^2.0.0" + "schema-utils" "^3.0.0" + +"rc-config-loader@^4.0.0": + "integrity" "sha512-//LRTblJEcqbmmro1GCmZ39qZXD+JqzuD8Y5/IZU3Dhp3A1Yr0Xn68ks8MQ6qKfKvYCWDveUmRDKDA40c+sCXw==" + "resolved" "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "debug" "^4.1.1" + "js-yaml" "^4.0.0" + "json5" "^2.1.2" + "require-from-string" "^2.0.2" + +"rc@^1.2.8": + "integrity" "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==" + "resolved" "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" + "version" "1.2.8" + dependencies: + "deep-extend" "^0.6.0" + "ini" "~1.3.0" + "minimist" "^1.2.0" + "strip-json-comments" "~2.0.1" + +"read-cache@^1.0.0": + "integrity" "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=" + "resolved" "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "pify" "^2.3.0" + +"read-package-json-fast@^2.0.1": + "integrity" "sha512-5fyFUyO9B799foVk4n6ylcoAktG/FbE3jwRKxvwaeSrIunaoMc0u81dzXxjeAFKOce7O5KncdfwpGvvs6r5PsQ==" + "resolved" "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "json-parse-even-better-errors" "^2.3.0" + "npm-normalize-package-bin" "^1.0.1" + +"readable-stream@^2.0.1": + "integrity" "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==" + "resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + "version" "2.3.7" + 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" + +"readable-stream@^2.0.2": + "integrity" "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==" + "resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + "version" "2.3.7" + 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" + +"readable-stream@^2.0.6": + "integrity" "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==" + "resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + "version" "2.3.7" + 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" + +"readable-stream@^3.0.6", "readable-stream@^3.4.0", "readable-stream@^3.6.0": + "integrity" "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==" + "resolved" "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + "version" "3.6.0" + dependencies: + "inherits" "^2.0.3" + "string_decoder" "^1.1.1" + "util-deprecate" "^1.0.1" + +"readdirp@^2.2.1": + "integrity" "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==" + "resolved" "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz" + "version" "2.2.1" + dependencies: + "graceful-fs" "^4.1.11" + "micromatch" "^3.1.10" + "readable-stream" "^2.0.2" + +"readdirp@~3.6.0": + "integrity" "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==" + "resolved" "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + "version" "3.6.0" + dependencies: + "picomatch" "^2.2.1" + +"reflect-metadata@^0.1.2": + "integrity" "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + "resolved" "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" + "version" "0.1.13" + +"regenerate-unicode-properties@^8.2.0": + "integrity" "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==" + "resolved" "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz" + "version" "8.2.0" + dependencies: + "regenerate" "^1.4.0" + +"regenerate@^1.4.0": + "integrity" "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + "resolved" "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + "version" "1.4.2" + +"regenerator-runtime@^0.13.4", "regenerator-runtime@0.13.7": + "integrity" "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + "resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz" + "version" "0.13.7" + +"regenerator-transform@^0.14.2": + "integrity" "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==" + "resolved" "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz" + "version" "0.14.5" + dependencies: + "@babel/runtime" "^7.8.4" + +"regex-not@^1.0.0", "regex-not@^1.0.2": + "integrity" "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==" + "resolved" "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "extend-shallow" "^3.0.2" + "safe-regex" "^1.1.0" + +"regex-parser@^2.2.11": + "integrity" "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" + "resolved" "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz" + "version" "2.2.11" + +"regexp.prototype.flags@^1.2.0": + "integrity" "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==" + "resolved" "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz" + "version" "1.3.1" + dependencies: + "call-bind" "^1.0.2" + "define-properties" "^1.1.3" + +"regexpu-core@^4.7.1": + "integrity" "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==" + "resolved" "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz" + "version" "4.7.1" + dependencies: + "regenerate" "^1.4.0" + "regenerate-unicode-properties" "^8.2.0" + "regjsgen" "^0.5.1" + "regjsparser" "^0.6.4" + "unicode-match-property-ecmascript" "^1.0.4" + "unicode-match-property-value-ecmascript" "^1.2.0" + +"registry-auth-token@^4.0.0": + "integrity" "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==" + "resolved" "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz" + "version" "4.2.1" + dependencies: + "rc" "^1.2.8" + +"registry-url@^5.0.0": + "integrity" "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==" + "resolved" "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz" + "version" "5.1.0" + dependencies: + "rc" "^1.2.8" + +"regjsgen@^0.5.1": + "integrity" "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==" + "resolved" "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz" + "version" "0.5.2" + +"regjsparser@^0.6.4": + "integrity" "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==" + "resolved" "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz" + "version" "0.6.9" + dependencies: + "jsesc" "~0.5.0" + +"remote-git-tags@^3.0.0": + "integrity" "sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==" + "resolved" "https://registry.npmjs.org/remote-git-tags/-/remote-git-tags-3.0.0.tgz" + "version" "3.0.0" + +"remove-trailing-separator@^1.0.1": + "integrity" "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + "resolved" "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" + "version" "1.1.0" + +"repeat-element@^1.1.2": + "integrity" "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==" + "resolved" "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz" + "version" "1.1.4" + +"repeat-string@^1.6.1": + "integrity" "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "resolved" "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" + "version" "1.6.1" + +"request@^2.88.2": + "integrity" "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==" + "resolved" "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + "version" "2.88.2" + 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" + +"require-directory@^2.1.1": + "integrity" "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "resolved" "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + "version" "2.1.1" + +"require-from-string@^2.0.2": + "integrity" "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + "resolved" "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + "version" "2.0.2" + +"require-main-filename@^2.0.0": + "integrity" "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "resolved" "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + "version" "2.0.0" + +"requires-port@^1.0.0": + "integrity" "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + "resolved" "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + "version" "1.0.0" + +"resolve-cwd@^2.0.0": + "integrity" "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=" + "resolved" "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "resolve-from" "^3.0.0" + +"resolve-from@^3.0.0": + "integrity" "sha1-six699nWiBvItuZTM17rywoYh0g=" + "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" + "version" "3.0.0" + +"resolve-from@^4.0.0": + "integrity" "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + "version" "4.0.0" + +"resolve-url-loader@4.0.0": + "integrity" "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==" + "resolved" "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "adjust-sourcemap-loader" "^4.0.0" + "convert-source-map" "^1.7.0" + "loader-utils" "^2.0.0" + "postcss" "^7.0.35" + "source-map" "0.6.1" + +"resolve-url@^0.2.1": + "integrity" "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "resolved" "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz" + "version" "0.2.1" + +"resolve@^1.1.7", "resolve@^1.14.2", "resolve@^1.3.2", "resolve@1.20.0": + "integrity" "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==" + "resolved" "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" + "version" "1.20.0" + dependencies: + "is-core-module" "^2.2.0" + "path-parse" "^1.0.6" + +"responselike@^1.0.2": + "integrity" "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=" + "resolved" "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "lowercase-keys" "^1.0.0" + +"restore-cursor@^3.1.0": + "integrity" "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==" + "resolved" "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "onetime" "^5.1.0" + "signal-exit" "^3.0.2" + +"ret@~0.1.10": + "integrity" "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "resolved" "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" + "version" "0.1.15" + +"retry@^0.12.0": + "integrity" "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + "resolved" "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + "version" "0.12.0" + +"reusify@^1.0.4": + "integrity" "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "resolved" "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + "version" "1.0.4" + +"rfdc@^1.1.4": + "integrity" "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + "resolved" "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" + "version" "1.3.0" + +"rgb-regex@^1.0.1": + "integrity" "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + "resolved" "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz" + "version" "1.0.1" + +"rgba-regex@^1.0.0": + "integrity" "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + "resolved" "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz" + "version" "1.0.0" + +"rimraf@^2.6.3": + "integrity" "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==" + "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" + "version" "2.7.1" + dependencies: + "glob" "^7.1.3" + +"rimraf@^3.0.0", "rimraf@^3.0.2", "rimraf@3.0.2": + "integrity" "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==" + "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "glob" "^7.1.3" + +"run-async@^2.4.0": + "integrity" "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + "resolved" "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" + "version" "2.4.1" + +"run-parallel@^1.1.9": + "integrity" "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==" + "resolved" "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "queue-microtask" "^1.2.2" + +"rw@1": + "integrity" "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + "resolved" "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz" + "version" "1.3.3" + +"rxjs@^6.5.3", "rxjs@^6.5.5", "rxjs@^6.6.6", "rxjs@>=6.5.3", "rxjs@~6.6.0", "rxjs@6.6.7": + "integrity" "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==" + "resolved" "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" + "version" "6.6.7" + dependencies: + "tslib" "^1.9.0" + +"safe-buffer@^5.0.1", "safe-buffer@^5.1.0", "safe-buffer@^5.1.2", "safe-buffer@>=5.1.0", "safe-buffer@~5.1.0", "safe-buffer@~5.1.1", "safe-buffer@5.1.2": + "integrity" "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + "version" "5.1.2" + +"safe-buffer@~5.2.0": + "integrity" "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "resolved" "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + "version" "5.2.1" + +"safe-regex@^1.1.0": + "integrity" "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=" + "resolved" "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" + "version" "1.1.0" + dependencies: + "ret" "~0.1.10" + +"safer-buffer@^2.0.2", "safer-buffer@^2.1.0", "safer-buffer@^2.1.2", "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", "safer-buffer@~2.1.0": + "integrity" "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "resolved" "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + "version" "2.1.2" + +"sass-loader@11.0.1": + "integrity" "sha512-Vp1LcP4slTsTNLEiDkTcm8zGN/XYYrZz2BZybQbliWA8eXveqA/AxsEjllQTpJbg2MzCsx/qNO48sHdZtOaxTw==" + "resolved" "https://registry.npmjs.org/sass-loader/-/sass-loader-11.0.1.tgz" + "version" "11.0.1" + dependencies: + "klona" "^2.0.4" + "neo-async" "^2.6.2" + +"sass@^1.3.0", "sass@1.32.12": + "integrity" "sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA==" + "resolved" "https://registry.npmjs.org/sass/-/sass-1.32.12.tgz" + "version" "1.32.12" + dependencies: + "chokidar" ">=3.0.0 <4.0.0" + +"save@^2.4.0": + "integrity" "sha512-wd5L2uVnsKYkIUaK6i8Ie66IOHaI328gMF0MPuTJtYOjXgUolC33LSIk7Qr8WVA55QHaGwfiVS8a7EFIeGOR3w==" + "resolved" "https://registry.npmjs.org/save/-/save-2.4.0.tgz" + "version" "2.4.0" + dependencies: + "async" "^2.6.2" + "event-stream" "^4.0.1" + "lodash.assign" "^4.2.0" + "mingo" "1" + +"sax@^1.2.4", "sax@~1.2.4": + "integrity" "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "resolved" "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" + "version" "1.2.4" + +"schema-utils@^1.0.0": + "integrity" "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==" + "resolved" "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "ajv" "^6.1.0" + "ajv-errors" "^1.0.0" + "ajv-keywords" "^3.1.0" + +"schema-utils@^2.6.5", "schema-utils@^2.7.0": + "integrity" "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==" + "resolved" "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" + "version" "2.7.1" + dependencies: + "@types/json-schema" "^7.0.5" + "ajv" "^6.12.4" + "ajv-keywords" "^3.5.2" + +"schema-utils@^3.0.0": + "integrity" "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==" + "resolved" "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "@types/json-schema" "^7.0.6" + "ajv" "^6.12.5" + "ajv-keywords" "^3.5.2" + +"select-hose@^2.0.0": + "integrity" "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + "resolved" "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + "version" "2.0.0" + +"selfsigned@^1.10.8": + "integrity" "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==" + "resolved" "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz" + "version" "1.10.11" + dependencies: + "node-forge" "^0.10.0" + +"semver-diff@^3.1.1": + "integrity" "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==" + "resolved" "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz" + "version" "3.1.1" + dependencies: + "semver" "^6.3.0" + +"semver-utils@^1.1.4": + "integrity" "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==" + "resolved" "https://registry.npmjs.org/semver-utils/-/semver-utils-1.1.4.tgz" + "version" "1.1.4" + +"semver@^5.4.1": + "integrity" "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" + "version" "5.7.1" + +"semver@^5.5.0": + "integrity" "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" + "version" "5.7.1" + +"semver@^5.6.0": + "integrity" "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "resolved" "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" + "version" "5.7.1" + +"semver@^6.0.0": + "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + "version" "6.3.0" + +"semver@^6.1.1": + "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + "version" "6.3.0" + +"semver@^6.1.2": + "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + "version" "6.3.0" + +"semver@^6.2.0": + "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + "version" "6.3.0" + +"semver@^6.3.0": + "integrity" "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "resolved" "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + "version" "6.3.0" + +"semver@^7.0.0", "semver@^7.1.1", "semver@^7.3.2", "semver@^7.3.4", "semver@^7.3.5", "semver@7.3.5": + "integrity" "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==" + "resolved" "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" + "version" "7.3.5" + dependencies: + "lru-cache" "^6.0.0" + +"semver@7.0.0": + "integrity" "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + "resolved" "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" + "version" "7.0.0" + +"send@0.17.1": + "integrity" "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==" + "resolved" "https://registry.npmjs.org/send/-/send-0.17.1.tgz" + "version" "0.17.1" + 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" + +"serialize-javascript@^5.0.1": + "integrity" "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==" + "resolved" "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "randombytes" "^2.1.0" + +"serve-index@^1.9.1": + "integrity" "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=" + "resolved" "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + "version" "1.9.1" + dependencies: + "accepts" "~1.3.4" + "batch" "0.6.1" + "debug" "2.6.9" + "escape-html" "~1.0.3" + "http-errors" "~1.6.2" + "mime-types" "~2.1.17" + "parseurl" "~1.3.2" + +"serve-static@1.14.1": + "integrity" "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==" + "resolved" "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz" + "version" "1.14.1" + dependencies: + "encodeurl" "~1.0.2" + "escape-html" "~1.0.3" + "parseurl" "~1.3.3" + "send" "0.17.1" + +"set-blocking@^2.0.0", "set-blocking@~2.0.0": + "integrity" "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "resolved" "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + "version" "2.0.0" + +"set-value@^2.0.0", "set-value@^2.0.1": + "integrity" "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==" + "resolved" "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "extend-shallow" "^2.0.1" + "is-extendable" "^0.1.1" + "is-plain-object" "^2.0.3" + "split-string" "^3.0.1" + +"setprototypeof@1.1.0": + "integrity" "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "resolved" "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + "version" "1.1.0" + +"setprototypeof@1.1.1": + "integrity" "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "resolved" "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz" + "version" "1.1.1" + +"shallow-clone@^3.0.0": + "integrity" "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==" + "resolved" "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "kind-of" "^6.0.2" + +"shebang-command@^1.2.0": + "integrity" "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=" + "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" + "version" "1.2.0" + dependencies: + "shebang-regex" "^1.0.0" + +"shebang-regex@^1.0.0": + "integrity" "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" + "version" "1.0.0" + +"signal-exit@^3.0.0", "signal-exit@^3.0.2": + "integrity" "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "resolved" "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz" + "version" "3.0.3" + +"sisteransi@^1.0.5": + "integrity" "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + "resolved" "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + "version" "1.0.5" + +"slash@^3.0.0": + "integrity" "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + "resolved" "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + "version" "3.0.0" + +"smart-buffer@^4.1.0": + "integrity" "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + "resolved" "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz" + "version" "4.1.0" + +"snapdragon-node@^2.0.1": + "integrity" "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==" + "resolved" "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "define-property" "^1.0.0" + "isobject" "^3.0.0" + "snapdragon-util" "^3.0.1" + +"snapdragon-util@^3.0.1": + "integrity" "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==" + "resolved" "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "kind-of" "^3.2.0" + +"snapdragon@^0.8.1": + "integrity" "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==" + "resolved" "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz" + "version" "0.8.2" + dependencies: + "base" "^0.11.1" + "debug" "^2.2.0" + "define-property" "^0.2.5" + "extend-shallow" "^2.0.1" + "map-cache" "^0.2.2" + "source-map" "^0.5.6" + "source-map-resolve" "^0.5.0" + "use" "^3.1.0" + +"socket.io-adapter@~2.1.0": + "integrity" "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" + "resolved" "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz" + "version" "2.1.0" + +"socket.io-parser@~4.0.3": + "integrity" "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==" + "resolved" "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz" + "version" "4.0.4" + dependencies: + "@types/component-emitter" "^1.2.10" + "component-emitter" "~1.3.0" + "debug" "~4.3.1" + +"socket.io@^3.1.0": + "integrity" "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==" + "resolved" "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz" + "version" "3.1.2" + dependencies: + "@types/cookie" "^0.4.0" + "@types/cors" "^2.8.8" + "@types/node" ">=10.0.0" + "accepts" "~1.3.4" + "base64id" "~2.0.0" + "debug" "~4.3.1" + "engine.io" "~4.1.0" + "socket.io-adapter" "~2.1.0" + "socket.io-parser" "~4.0.3" + +"sockjs-client@^1.5.0": + "integrity" "sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==" + "resolved" "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.1.tgz" + "version" "1.5.1" + dependencies: + "debug" "^3.2.6" + "eventsource" "^1.0.7" + "faye-websocket" "^0.11.3" + "inherits" "^2.0.4" + "json3" "^3.3.3" + "url-parse" "^1.5.1" + +"sockjs@^0.3.21": + "integrity" "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==" + "resolved" "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz" + "version" "0.3.21" + dependencies: + "faye-websocket" "^0.11.3" + "uuid" "^3.4.0" + "websocket-driver" "^0.7.4" + +"socks-proxy-agent@^5.0.0": + "integrity" "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==" + "resolved" "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "agent-base" "^6.0.2" + "debug" "4" + "socks" "^2.3.3" + +"socks-proxy-agent@^6.0.0": + "integrity" "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==" + "resolved" "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz" + "version" "6.1.1" + dependencies: + "agent-base" "^6.0.2" + "debug" "^4.3.1" + "socks" "^2.6.1" + +"socks@^2.3.3", "socks@^2.6.1": + "integrity" "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==" + "resolved" "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz" + "version" "2.6.1" + dependencies: + "ip" "^1.1.5" + "smart-buffer" "^4.1.0" + +"source-list-map@^2.0.0", "source-list-map@^2.0.1": + "integrity" "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + "resolved" "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" + "version" "2.0.1" + +"source-map-js@^0.6.2": + "integrity" "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + "resolved" "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz" + "version" "0.6.2" + +"source-map-loader@2.0.1": + "integrity" "sha512-UzOTTQhoNPeTNzOxwFw220RSRzdGSyH4lpNyWjR7Qm34P4/N0W669YSUFdH07+YNeN75h765XLHmNsF/bm97RQ==" + "resolved" "https://registry.npmjs.org/source-map-loader/-/source-map-loader-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "abab" "^2.0.5" + "iconv-lite" "^0.6.2" + "source-map-js" "^0.6.2" + +"source-map-resolve@^0.5.0": + "integrity" "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==" + "resolved" "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz" + "version" "0.5.3" + dependencies: + "atob" "^2.1.2" + "decode-uri-component" "^0.2.0" + "resolve-url" "^0.2.1" + "source-map-url" "^0.4.0" + "urix" "^0.1.0" + +"source-map-resolve@^0.5.2": + "integrity" "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==" + "resolved" "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz" + "version" "0.5.3" + dependencies: + "atob" "^2.1.2" + "decode-uri-component" "^0.2.0" + "resolve-url" "^0.2.1" + "source-map-url" "^0.4.0" + "urix" "^0.1.0" + +"source-map-resolve@^0.6.0": + "integrity" "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==" + "resolved" "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz" + "version" "0.6.0" + dependencies: + "atob" "^2.1.2" + "decode-uri-component" "^0.2.0" + +"source-map-support@^0.5.21": + "integrity" "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==" + "resolved" "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + "version" "0.5.21" + dependencies: + "buffer-from" "^1.0.0" + "source-map" "^0.6.0" + +"source-map-support@^0.5.5", "source-map-support@~0.5.19", "source-map-support@0.5.19": + "integrity" "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==" + "resolved" "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz" + "version" "0.5.19" + dependencies: + "buffer-from" "^1.0.0" + "source-map" "^0.6.0" + +"source-map-url@^0.4.0": + "integrity" "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" + "resolved" "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz" + "version" "0.4.1" + +"source-map@^0.5.0": + "integrity" "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + "version" "0.5.7" + +"source-map@^0.5.6": + "integrity" "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + "version" "0.5.7" + +"source-map@^0.6.0": + "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "version" "0.6.1" + +"source-map@^0.6.1", "source-map@0.6.1": + "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "version" "0.6.1" + +"source-map@^0.7.3", "source-map@~0.7.2", "source-map@0.7.3": + "integrity" "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" + "version" "0.7.3" + +"source-map@~0.6.0": + "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "version" "0.6.1" + +"source-map@~0.6.1": + "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "resolved" "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + "version" "0.6.1" + +"sourcemap-codec@^1.4.4", "sourcemap-codec@^1.4.8": + "integrity" "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "resolved" "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" + "version" "1.4.8" + +"spawn-please@^1.0.0": + "integrity" "sha512-Kz33ip6NRNKuyTRo3aDWyWxeGeM0ORDO552Fs6E1nj4pLWPkl37SrRtTnq+MEopVaqgmaO6bAvVS+v64BJ5M/A==" + "resolved" "https://registry.npmjs.org/spawn-please/-/spawn-please-1.0.0.tgz" + "version" "1.0.0" + +"spdy-transport@^3.0.0": + "integrity" "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==" + "resolved" "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "debug" "^4.1.0" + "detect-node" "^2.0.4" + "hpack.js" "^2.1.6" + "obuf" "^1.1.2" + "readable-stream" "^3.0.6" + "wbuf" "^1.7.3" + +"spdy@^4.0.2": + "integrity" "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==" + "resolved" "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + "version" "4.0.2" + dependencies: + "debug" "^4.1.0" + "handle-thing" "^2.0.0" + "http-deceiver" "^1.2.7" + "select-hose" "^2.0.0" + "spdy-transport" "^3.0.0" + +"split-string@^3.0.1", "split-string@^3.0.2": + "integrity" "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==" + "resolved" "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "extend-shallow" "^3.0.0" + +"split@^1.0.1": + "integrity" "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==" + "resolved" "https://registry.npmjs.org/split/-/split-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "through" "2" + +"sshpk@^1.7.0": + "integrity" "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==" + "resolved" "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz" + "version" "1.16.1" + 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" + +"ssri@^8.0.0", "ssri@^8.0.1": + "integrity" "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==" + "resolved" "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" + "version" "8.0.1" + dependencies: + "minipass" "^3.1.1" + +"stable@^0.1.8": + "integrity" "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + "resolved" "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" + "version" "0.1.8" + +"static-extend@^0.1.1": + "integrity" "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=" + "resolved" "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz" + "version" "0.1.2" + dependencies: + "define-property" "^0.2.5" + "object-copy" "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", "statuses@~1.5.0": + "integrity" "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "resolved" "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + "version" "1.5.0" + +"stream-combiner@^0.2.2": + "integrity" "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=" + "resolved" "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz" + "version" "0.2.2" + dependencies: + "duplexer" "~0.1.1" + "through" "~2.3.4" + +"streamroller@^2.2.4": + "integrity" "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==" + "resolved" "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz" + "version" "2.2.4" + dependencies: + "date-format" "^2.1.0" + "debug" "^4.1.1" + "fs-extra" "^8.1.0" + +"string_decoder@^1.1.1": + "integrity" "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==" + "resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + "version" "1.3.0" + dependencies: + "safe-buffer" "~5.2.0" + +"string_decoder@~1.1.1": + "integrity" "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==" + "resolved" "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "safe-buffer" "~5.1.0" + +"string-width@^1.0.1": + "integrity" "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + "version" "1.0.2" + dependencies: + "code-point-at" "^1.0.0" + "is-fullwidth-code-point" "^1.0.0" + "strip-ansi" "^3.0.0" + +"string-width@^1.0.2 || 2": + "integrity" "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "is-fullwidth-code-point" "^2.0.0" + "strip-ansi" "^4.0.0" + +"string-width@^3.0.0": + "integrity" "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "emoji-regex" "^7.0.1" + "is-fullwidth-code-point" "^2.0.0" + "strip-ansi" "^5.1.0" + +"string-width@^3.1.0": + "integrity" "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "emoji-regex" "^7.0.1" + "is-fullwidth-code-point" "^2.0.0" + "strip-ansi" "^5.1.0" + +"string-width@^4.0.0", "string-width@^4.1.0", "string-width@^4.2.0", "string-width@^4.2.2", "string-width@^4.2.3": + "integrity" "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==" + "resolved" "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + "version" "4.2.3" + dependencies: + "emoji-regex" "^8.0.0" + "is-fullwidth-code-point" "^3.0.0" + "strip-ansi" "^6.0.1" + +"strip-ansi@^3.0.0", "strip-ansi@^3.0.1": + "integrity" "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + "version" "3.0.1" + dependencies: + "ansi-regex" "^2.0.0" + +"strip-ansi@^4.0.0": + "integrity" "sha1-qEeQIusaw2iocTibY1JixQXuNo8=" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" + "version" "4.0.0" + dependencies: + "ansi-regex" "^3.0.0" + +"strip-ansi@^5.0.0", "strip-ansi@^5.1.0", "strip-ansi@^5.2.0": + "integrity" "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" + "version" "5.2.0" + dependencies: + "ansi-regex" "^4.1.0" + +"strip-ansi@^6.0.0", "strip-ansi@^6.0.1": + "integrity" "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "ansi-regex" "^5.0.1" + +"strip-eof@^1.0.0": + "integrity" "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "resolved" "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz" + "version" "1.0.0" + +"strip-json-comments@~2.0.1": + "integrity" "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + "version" "2.0.1" + +"style-loader@2.0.0": + "integrity" "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==" + "resolved" "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "loader-utils" "^2.0.0" + "schema-utils" "^3.0.0" + +"stylehacks@^5.0.1": + "integrity" "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==" + "resolved" "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "browserslist" "^4.16.0" + "postcss-selector-parser" "^6.0.4" + +"stylus-loader@5.0.0": + "integrity" "sha512-1OaGgixTgC8IAaMCodZXg7XYsfP1qU0UzTHDyPaWACUh34j9geJL4iA583tFJDOtfNUOfDLaBpUywc5MicQ1aA==" + "resolved" "https://registry.npmjs.org/stylus-loader/-/stylus-loader-5.0.0.tgz" + "version" "5.0.0" + dependencies: + "fast-glob" "^3.2.5" + "klona" "^2.0.4" + "normalize-path" "^3.0.0" + +"stylus@>=0.52.4", "stylus@0.54.8": + "integrity" "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==" + "resolved" "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz" + "version" "0.54.8" + dependencies: + "css-parse" "~2.0.0" + "debug" "~3.1.0" + "glob" "^7.1.6" + "mkdirp" "~1.0.4" + "safer-buffer" "^2.1.2" + "sax" "~1.2.4" + "semver" "^6.3.0" + "source-map" "^0.7.3" + +"supports-color@^5.3.0": + "integrity" "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + "version" "5.5.0" + dependencies: + "has-flag" "^3.0.0" + +"supports-color@^6.1.0": + "integrity" "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz" + "version" "6.1.0" + dependencies: + "has-flag" "^3.0.0" + +"supports-color@^7.0.0": + "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "has-flag" "^4.0.0" + +"supports-color@^7.1.0": + "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "has-flag" "^4.0.0" + +"svgo@^2.3.0": + "integrity" "sha512-riDDIQgXpEnn0BEl9Gvhh1LNLIyiusSpt64IR8upJu7MwxnzetmF/Y57pXQD2NMX2lVyMRzXt5f2M5rO4wG7Dw==" + "resolved" "https://registry.npmjs.org/svgo/-/svgo-2.3.1.tgz" + "version" "2.3.1" + dependencies: + "@trysound/sax" "0.1.1" + "chalk" "^4.1.0" + "commander" "^7.1.0" + "css-select" "^4.1.3" + "css-tree" "^1.1.2" + "csso" "^4.2.0" + "stable" "^0.1.8" + +"symbol-observable@4.0.0": + "integrity" "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==" + "resolved" "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz" + "version" "4.0.0" + +"tapable@^2.1.1", "tapable@^2.2.0": + "integrity" "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" + "resolved" "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz" + "version" "2.2.0" + +"tar@^6.0.2", "tar@^6.1.0", "tar@^6.1.2": + "integrity" "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==" + "resolved" "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" + "version" "6.1.11" + dependencies: + "chownr" "^2.0.0" + "fs-minipass" "^2.0.0" + "minipass" "^3.0.0" + "minizlib" "^2.1.1" + "mkdirp" "^1.0.3" + "yallist" "^4.0.0" + +"terser-webpack-plugin@^5.1.1", "terser-webpack-plugin@5.1.2": + "integrity" "sha512-6QhDaAiVHIQr5Ab3XUWZyDmrIPCHMiqJVljMF91YKyqwKkL5QHnYMkrMBy96v9Z7ev1hGhSEw1HQZc2p/s5Z8Q==" + "resolved" "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.2.tgz" + "version" "5.1.2" + dependencies: + "jest-worker" "^26.6.2" + "p-limit" "^3.1.0" + "schema-utils" "^3.0.0" + "serialize-javascript" "^5.0.1" + "source-map" "^0.6.1" + "terser" "^5.7.0" + +"terser@^5.7.0", "terser@5.7.0": + "integrity" "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==" + "resolved" "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz" + "version" "5.7.0" + dependencies: + "commander" "^2.20.0" + "source-map" "~0.7.2" + "source-map-support" "~0.5.19" + +"text-table@0.2.0": + "integrity" "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + "version" "0.2.0" + +"through@^2.3.6", "through@^2.3.8", "through@~2.3", "through@~2.3.4", "through@2": + "integrity" "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "resolved" "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + "version" "2.3.8" + +"thunky@^1.0.2": + "integrity" "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + "resolved" "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + "version" "1.1.0" + +"timsort@^0.3.0": + "integrity" "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + "resolved" "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz" + "version" "0.3.0" + +"tmp@^0.0.33": + "integrity" "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==" + "resolved" "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" + "version" "0.0.33" + dependencies: + "os-tmpdir" "~1.0.2" + +"tmp@^0.2.1": + "integrity" "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==" + "resolved" "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" + "version" "0.2.1" + dependencies: + "rimraf" "^3.0.0" + +"to-fast-properties@^2.0.0": + "integrity" "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "resolved" "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + "version" "2.0.0" + +"to-object-path@^0.3.0": + "integrity" "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=" + "resolved" "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz" + "version" "0.3.0" + dependencies: + "kind-of" "^3.0.2" + +"to-readable-stream@^1.0.0": + "integrity" "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + "resolved" "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz" + "version" "1.0.0" + +"to-regex-range@^2.1.0": + "integrity" "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=" + "resolved" "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz" + "version" "2.1.1" + dependencies: + "is-number" "^3.0.0" + "repeat-string" "^1.6.1" + +"to-regex-range@^5.0.1": + "integrity" "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" + "resolved" "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + "version" "5.0.1" + dependencies: + "is-number" "^7.0.0" + +"to-regex@^3.0.1", "to-regex@^3.0.2": + "integrity" "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==" + "resolved" "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "define-property" "^2.0.2" + "extend-shallow" "^3.0.2" + "regex-not" "^1.0.2" + "safe-regex" "^1.1.0" + +"toidentifier@1.0.0": + "integrity" "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "resolved" "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" + "version" "1.0.0" + +"tough-cookie@~2.5.0": + "integrity" "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==" + "resolved" "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + "version" "2.5.0" + dependencies: + "psl" "^1.1.28" + "punycode" "^2.1.1" + +"tree-kill@1.2.2": + "integrity" "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + "resolved" "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" + "version" "1.2.2" + +"tslib@^1.10.0": + "integrity" "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "resolved" "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + "version" "1.14.1" + +"tslib@^1.9.0": + "integrity" "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "resolved" "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + "version" "1.14.1" + +"tslib@^2.0.0", "tslib@^2.1.0", "tslib@^2.2.0": + "integrity" "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "resolved" "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz" + "version" "2.3.0" + +"tslib@2.2.0": + "integrity" "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + "resolved" "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz" + "version" "2.2.0" + +"tunnel-agent@^0.6.0": + "integrity" "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" + "resolved" "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + "version" "0.6.0" + dependencies: + "safe-buffer" "^5.0.1" + +"tweetnacl@^0.14.3", "tweetnacl@~0.14.0": + "integrity" "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "resolved" "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + "version" "0.14.5" + +"type-fest@^0.20.2": + "integrity" "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + "version" "0.20.2" + +"type-fest@^0.21.3": + "integrity" "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + "version" "0.21.3" + +"type-is@~1.6.17", "type-is@~1.6.18": + "integrity" "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==" + "resolved" "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + "version" "1.6.18" + dependencies: + "media-typer" "0.3.0" + "mime-types" "~2.1.24" + +"typedarray-to-buffer@^3.1.5": + "integrity" "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==" + "resolved" "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" + "version" "3.1.5" + dependencies: + "is-typedarray" "^1.0.0" + +"typescript@>=4.2.3 <4.3", "typescript@~4.2.3", "typescript@4.2.4": + "integrity" "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" + "resolved" "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz" + "version" "4.2.4" + +"ua-parser-js@^0.7.28": + "integrity" "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==" + "resolved" "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz" + "version" "0.7.28" + +"unicode-canonical-property-names-ecmascript@^1.0.4": + "integrity" "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" + "resolved" "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz" + "version" "1.0.4" + +"unicode-match-property-ecmascript@^1.0.4": + "integrity" "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==" + "resolved" "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz" + "version" "1.0.4" + dependencies: + "unicode-canonical-property-names-ecmascript" "^1.0.4" + "unicode-property-aliases-ecmascript" "^1.0.4" + +"unicode-match-property-value-ecmascript@^1.2.0": + "integrity" "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==" + "resolved" "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz" + "version" "1.2.0" + +"unicode-property-aliases-ecmascript@^1.0.4": + "integrity" "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" + "resolved" "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz" + "version" "1.1.0" + +"union-value@^1.0.0": + "integrity" "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==" + "resolved" "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "arr-union" "^3.1.0" + "get-value" "^2.0.6" + "is-extendable" "^0.1.1" + "set-value" "^2.0.1" + +"uniq@^1.0.1": + "integrity" "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + "resolved" "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz" + "version" "1.0.1" + +"uniqs@^2.0.0": + "integrity" "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + "resolved" "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz" + "version" "2.0.0" + +"unique-filename@^1.1.1": + "integrity" "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==" + "resolved" "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" + "version" "1.1.1" + dependencies: + "unique-slug" "^2.0.0" + +"unique-slug@^2.0.0": + "integrity" "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==" + "resolved" "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "imurmurhash" "^0.1.4" + +"unique-string@^2.0.0": + "integrity" "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==" + "resolved" "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "crypto-random-string" "^2.0.0" + +"universalify@^0.1.0": + "integrity" "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "resolved" "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" + "version" "0.1.2" + +"unpipe@~1.0.0", "unpipe@1.0.0": + "integrity" "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "resolved" "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + "version" "1.0.0" + +"unset-value@^1.0.0": + "integrity" "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=" + "resolved" "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz" + "version" "1.0.0" + dependencies: + "has-value" "^0.3.1" + "isobject" "^3.0.0" + +"upath@^1.1.1": + "integrity" "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" + "resolved" "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" + "version" "1.2.0" + +"update-notifier@^5.1.0": + "integrity" "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==" + "resolved" "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz" + "version" "5.1.0" + dependencies: + "boxen" "^5.0.0" + "chalk" "^4.1.0" + "configstore" "^5.0.1" + "has-yarn" "^2.1.0" + "import-lazy" "^2.1.0" + "is-ci" "^2.0.0" + "is-installed-globally" "^0.4.0" + "is-npm" "^5.0.0" + "is-yarn-global" "^0.3.0" + "latest-version" "^5.1.0" + "pupa" "^2.1.1" + "semver" "^7.3.4" + "semver-diff" "^3.1.1" + "xdg-basedir" "^4.0.0" + +"uri-js@^4.2.2": + "integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" + "resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + "version" "4.4.1" + dependencies: + "punycode" "^2.1.0" + +"urix@^0.1.0": + "integrity" "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "resolved" "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz" + "version" "0.1.0" + +"url-parse-lax@^3.0.0": + "integrity" "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=" + "resolved" "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "prepend-http" "^2.0.0" + +"url-parse@^1.4.3", "url-parse@^1.5.1": + "integrity" "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==" + "resolved" "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz" + "version" "1.5.1" + dependencies: + "querystringify" "^2.1.1" + "requires-port" "^1.0.0" + +"url@^0.11.0": + "integrity" "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=" + "resolved" "https://registry.npmjs.org/url/-/url-0.11.0.tgz" + "version" "0.11.0" + dependencies: + "punycode" "1.3.2" + "querystring" "0.2.0" + +"use@^3.1.0": + "integrity" "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + "resolved" "https://registry.npmjs.org/use/-/use-3.1.1.tgz" + "version" "3.1.1" + +"util-deprecate@^1.0.1", "util-deprecate@^1.0.2", "util-deprecate@~1.0.1": + "integrity" "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + "version" "1.0.2" + +"utils-merge@1.0.1": + "integrity" "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "resolved" "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + "version" "1.0.1" + +"uuid@^3.3.2", "uuid@^3.4.0": + "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + "version" "3.4.0" + +"uuid@8.3.2": + "integrity" "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + "version" "8.3.2" + +"validate-npm-package-name@^3.0.0": + "integrity" "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=" + "resolved" "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "builtins" "^1.0.3" + +"vary@^1", "vary@~1.1.2": + "integrity" "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "resolved" "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + "version" "1.1.2" + +"vendors@^1.0.3": + "integrity" "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" + "resolved" "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz" + "version" "1.0.4" + +"verror@1.10.0": + "integrity" "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=" + "resolved" "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + "version" "1.10.0" + dependencies: + "assert-plus" "^1.0.0" + "core-util-is" "1.0.2" + "extsprintf" "^1.2.0" + +"void-elements@^2.0.0": + "integrity" "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + "resolved" "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" + "version" "2.0.1" + +"watchpack@^2.2.0": + "integrity" "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==" + "resolved" "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz" + "version" "2.2.0" + dependencies: + "glob-to-regexp" "^0.4.1" + "graceful-fs" "^4.1.2" + +"wbuf@^1.1.0", "wbuf@^1.7.3": + "integrity" "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==" + "resolved" "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + "version" "1.7.3" + dependencies: + "minimalistic-assert" "^1.0.0" + +"wcwidth@^1.0.1": + "integrity" "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=" + "resolved" "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "defaults" "^1.0.3" + +"webpack-dev-middleware@^3.7.2": + "integrity" "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==" + "resolved" "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz" + "version" "3.7.3" + dependencies: + "memory-fs" "^0.4.1" + "mime" "^2.4.4" + "mkdirp" "^0.5.1" + "range-parser" "^1.2.1" + "webpack-log" "^2.0.0" + +"webpack-dev-middleware@4.1.0": + "integrity" "sha512-mpa/FY+DiBu5+r5JUIyTCYWRfkWgyA3/OOE9lwfzV9S70A4vJYLsVRKj5rMFEsezBroy2FmPyQ8oBRVW8QmK1A==" + "resolved" "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "colorette" "^1.2.1" + "mem" "^8.0.0" + "memfs" "^3.2.0" + "mime-types" "^2.1.28" + "range-parser" "^1.2.1" + "schema-utils" "^3.0.0" + +"webpack-dev-server@^3.1.4", "webpack-dev-server@3.11.2": + "integrity" "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==" + "resolved" "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz" + "version" "3.11.2" + dependencies: + "ansi-html" "0.0.7" + "bonjour" "^3.5.0" + "chokidar" "^2.1.8" + "compression" "^1.7.4" + "connect-history-api-fallback" "^1.6.0" + "debug" "^4.1.1" + "del" "^4.1.1" + "express" "^4.17.1" + "html-entities" "^1.3.1" + "http-proxy-middleware" "0.19.1" + "import-local" "^2.0.0" + "internal-ip" "^4.3.0" + "ip" "^1.1.5" + "is-absolute-url" "^3.0.3" + "killable" "^1.0.1" + "loglevel" "^1.6.8" + "opn" "^5.5.0" + "p-retry" "^3.0.1" + "portfinder" "^1.0.26" + "schema-utils" "^1.0.0" + "selfsigned" "^1.10.8" + "semver" "^6.3.0" + "serve-index" "^1.9.1" + "sockjs" "^0.3.21" + "sockjs-client" "^1.5.0" + "spdy" "^4.0.2" + "strip-ansi" "^3.0.1" + "supports-color" "^6.1.0" + "url" "^0.11.0" + "webpack-dev-middleware" "^3.7.2" + "webpack-log" "^2.0.0" + "ws" "^6.2.1" + "yargs" "^13.3.2" + +"webpack-log@^2.0.0": + "integrity" "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==" + "resolved" "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "ansi-colors" "^3.0.0" + "uuid" "^3.3.2" + +"webpack-merge@5.7.3": + "integrity" "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==" + "resolved" "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz" + "version" "5.7.3" + dependencies: + "clone-deep" "^4.0.1" + "wildcard" "^2.0.0" + +"webpack-sources@^1.1.0", "webpack-sources@^1.2.0", "webpack-sources@^1.3.0": + "integrity" "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==" + "resolved" "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" + "version" "1.4.3" + dependencies: + "source-list-map" "^2.0.0" + "source-map" "~0.6.1" + +"webpack-sources@^2.3.0": + "integrity" "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==" + "resolved" "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz" + "version" "2.3.0" + dependencies: + "source-list-map" "^2.0.1" + "source-map" "^0.6.1" + +"webpack-subresource-integrity@1.5.2": + "integrity" "sha512-GBWYBoyalbo5YClwWop9qe6Zclp8CIXYGIz12OPclJhIrSplDxs1Ls1JDMH8xBPPrg1T6ISaTW9Y6zOrwEiAzw==" + "resolved" "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz" + "version" "1.5.2" + dependencies: + "webpack-sources" "^1.3.0" + +"webpack@^4.0.0 || ^5.0.0", "webpack@^4.27.0 || ^5.0.0", "webpack@^4.4.0 || ^5.0.0", "webpack@^5.0.0", "webpack@^5.1.0", "webpack@^5.30.0", "webpack@>= 1.12.11 < 6", "webpack@>=2", "webpack@>=4.0.1", "webpack@5.39.1": + "integrity" "sha512-ulOvoNCh2PvTUa+zbpRuEb1VPeQnhxpnHleMPVVCq3QqnaFogjsLyps+o42OviQFoaGtTQYrUqDXu1QNkvUPzw==" + "resolved" "https://registry.npmjs.org/webpack/-/webpack-5.39.1.tgz" + "version" "5.39.1" + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.47" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/wasm-edit" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + "acorn" "^8.2.1" + "browserslist" "^4.14.5" + "chrome-trace-event" "^1.0.2" + "enhanced-resolve" "^5.8.0" + "es-module-lexer" "^0.4.0" + "eslint-scope" "5.1.1" + "events" "^3.2.0" + "glob-to-regexp" "^0.4.1" + "graceful-fs" "^4.2.4" + "json-parse-better-errors" "^1.0.2" + "loader-runner" "^4.2.0" + "mime-types" "^2.1.27" + "neo-async" "^2.6.2" + "schema-utils" "^3.0.0" + "tapable" "^2.1.1" + "terser-webpack-plugin" "^5.1.1" + "watchpack" "^2.2.0" + "webpack-sources" "^2.3.0" + +"websocket-driver@^0.7.4", "websocket-driver@>=0.5.1": + "integrity" "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==" + "resolved" "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + "version" "0.7.4" + dependencies: + "http-parser-js" ">=0.5.1" + "safe-buffer" ">=5.1.0" + "websocket-extensions" ">=0.1.1" + +"websocket-extensions@>=0.1.1": + "integrity" "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + "resolved" "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + "version" "0.1.4" + +"which-module@^2.0.0": + "integrity" "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "resolved" "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" + "version" "2.0.0" + +"which@^1.2.1", "which@^1.2.9": + "integrity" "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==" + "resolved" "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + "version" "1.3.1" + dependencies: + "isexe" "^2.0.0" + +"which@^2.0.2": + "integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" + "resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "isexe" "^2.0.0" + +"wide-align@^1.1.0", "wide-align@^1.1.2": + "integrity" "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==" + "resolved" "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz" + "version" "1.1.3" + dependencies: + "string-width" "^1.0.2 || 2" + +"widest-line@^3.1.0": + "integrity" "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==" + "resolved" "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz" + "version" "3.1.0" + dependencies: + "string-width" "^4.0.0" + +"wildcard@^2.0.0": + "integrity" "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + "resolved" "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz" + "version" "2.0.0" + +"wrap-ansi@^5.1.0": + "integrity" "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==" + "resolved" "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz" + "version" "5.1.0" + dependencies: + "ansi-styles" "^3.2.0" + "string-width" "^3.0.0" + "strip-ansi" "^5.0.0" + +"wrap-ansi@^7.0.0": + "integrity" "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==" + "resolved" "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + "version" "7.0.0" + dependencies: + "ansi-styles" "^4.0.0" + "string-width" "^4.1.0" + "strip-ansi" "^6.0.0" + +"wrappy@1": + "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "version" "1.0.2" + +"write-file-atomic@^3.0.0": + "integrity" "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==" + "resolved" "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" + "version" "3.0.3" + dependencies: + "imurmurhash" "^0.1.4" + "is-typedarray" "^1.0.0" + "signal-exit" "^3.0.2" + "typedarray-to-buffer" "^3.1.5" + +"ws@^6.2.1": + "integrity" "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==" + "resolved" "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz" + "version" "6.2.2" + dependencies: + "async-limiter" "~1.0.0" + +"ws@~7.4.2": + "integrity" "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "resolved" "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz" + "version" "7.4.6" + +"xdg-basedir@^4.0.0": + "integrity" "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + "resolved" "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz" + "version" "4.0.0" + +"y18n@^4.0.0": + "integrity" "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "resolved" "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + "version" "4.0.3" + +"y18n@^5.0.5": + "integrity" "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + "resolved" "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + "version" "5.0.8" + +"yallist@^4.0.0": + "integrity" "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "resolved" "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + "version" "4.0.0" + +"yaml@^1.10.0": + "integrity" "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "resolved" "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + "version" "1.10.2" + +"yargs-parser@^13.1.2": + "integrity" "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==" + "resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz" + "version" "13.1.2" + dependencies: + "camelcase" "^5.0.0" + "decamelize" "^1.2.0" + +"yargs-parser@^20.2.2": + "integrity" "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + "version" "20.2.9" + +"yargs@^13.3.2": + "integrity" "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==" + "resolved" "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz" + "version" "13.3.2" + dependencies: + "cliui" "^5.0.0" + "find-up" "^3.0.0" + "get-caller-file" "^2.0.1" + "require-directory" "^2.1.1" + "require-main-filename" "^2.0.0" + "set-blocking" "^2.0.0" + "string-width" "^3.0.0" + "which-module" "^2.0.0" + "y18n" "^4.0.0" + "yargs-parser" "^13.1.2" + +"yargs@^16.1.1": + "integrity" "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==" + "resolved" "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + "version" "16.2.0" + dependencies: + "cliui" "^7.0.2" + "escalade" "^3.1.1" + "get-caller-file" "^2.0.5" + "require-directory" "^2.1.1" + "string-width" "^4.2.0" + "y18n" "^5.0.5" + "yargs-parser" "^20.2.2" + +"yargs@^16.2.0": + "integrity" "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==" + "resolved" "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + "version" "16.2.0" + dependencies: + "cliui" "^7.0.2" + "escalade" "^3.1.1" + "get-caller-file" "^2.0.5" + "require-directory" "^2.1.1" + "string-width" "^4.2.0" + "y18n" "^5.0.5" + "yargs-parser" "^20.2.2" + +"yocto-queue@^0.1.0": + "integrity" "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "resolved" "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + "version" "0.1.0" + +"zone.js@~0.11.4": + "integrity" "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==" + "resolved" "https://registry.npmjs.org/zone.js/-/zone.js-0.11.4.tgz" + "version" "0.11.4" + dependencies: + "tslib" "^2.0.0" diff --git a/site-portal/go.mod b/site-portal/go.mod new file mode 100644 index 00000000..d3ef6e2d --- /dev/null +++ b/site-portal/go.mod @@ -0,0 +1,107 @@ +module github.com/FederatedAI/FedLCM/site-portal + +go 1.17 + +require ( + github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b + github.com/appleboy/gin-jwt/v2 v2.8.0 + github.com/gin-contrib/logger v0.2.2 + github.com/gin-gonic/gin v1.8.1 + github.com/hashicorp/go-version v1.6.0 + github.com/minio/minio-go/v7 v7.0.30 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.27.0 + github.com/satori/go.uuid v1.2.0 + github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.7.2 + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe + github.com/swaggo/gin-swagger v1.5.0 + github.com/swaggo/swag v1.8.3 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d + gorm.io/driver/postgres v1.3.7 + gorm.io/gorm v1.23.6 + k8s.io/apimachinery v0.24.2 + k8s.io/client-go v0.24.2 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.0 // indirect + github.com/goccy/go-json v0.9.8 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.7 // indirect + github.com/klauspost/cpuid/v2 v2.0.14 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.0 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 // indirect + golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/tools v0.1.11 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.24.2 // indirect + k8s.io/klog/v2 v2.70.0 // indirect + k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect + sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/site-portal/go.sum b/site-portal/go.sum new file mode 100644 index 00000000..06c0f4d3 --- /dev/null +++ b/site-portal/go.sum @@ -0,0 +1,2171 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b h1:2RoqwJw4QTJ/heCASun5F5m/kf6y86jIod/pgZ2MWYA= +github.com/FederatedAI/KubeFATE/k8s-deploy v0.0.0-20220526064123-d710a7dd448b/go.mod h1:3/tZ0DwSUKl06W2exOUDLk5D9plmtwjIoIhFtVZwpTY= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim v0.9.1/go.mod h1:Y/0uV2jUab5kBI7SQgl62at0AVX7uaruzADAVmxm3eM= +github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/appleboy/gin-jwt/v2 v2.8.0 h1:Glo7cb9eBR+hj8Y7WzgfkOlqCaNLjP+RV4dNO3fpdps= +github.com/appleboy/gin-jwt/v2 v2.8.0/go.mod h1:KsK7E8HTvRg3vOiumTsr/ntNTHbZ3IbHLe4Eto31p7k= +github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.2/go.mod h1:qpbpJ1jmlqsR9f2IyaLPsdkCdnt0rbDVqIDlhuu5tRY= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= +github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= +github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc= +github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0= +github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk= +github.com/gin-contrib/logger v0.2.2 h1:xIoUvRdmfID02X09wfq7wuWmevBTdMK1T6TQjbv5r+4= +github.com/gin-contrib/logger v0.2.2/go.mod h1:6uKBteCGZF6VtxSfO1MKWl7aEu1sPSOhwCEAFPFxnnI= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE= +github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= +github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.14 h1:QRqdp6bb9M9S5yyKeYteXKuoKE4p0tGlra81fKOpWH8= +github.com/klauspost/cpuid/v2 v2.0.14/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.30 h1:Re+qlwA+LB3mgFGYbztVPzlEjKtGzRVV5Sk38np858k= +github.com/minio/minio-go/v7 v7.0.30/go.mod h1:/sjRKkKIA75CKh1iu8E3qBy7ktBmCCDGII0zbXGwbUk= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= +github.com/rubenv/sql-migrate v1.1.1/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/gin-swagger v1.4.1/go.mod h1:hmJ1vPn+XjUvnbzjCdUAxVqgraxELxk8x5zAsjCE5mg= +github.com/swaggo/gin-swagger v1.5.0 h1:hlLbxPj6qvbtX2wpbsZuOIlcnPRCUDGccA0zMKVNpME= +github.com/swaggo/gin-swagger v1.5.0/go.mod h1:3mKpZClKx7mnUGsiwJeEkNhnr1VHMkMaTAXIoFYUXrA= +github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= +github.com/swaggo/swag v1.8.0/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s= +github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.starlark.net v0.0.0-20220302181546-5411bad688d1/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 h1:uBgVQYJLi/m8M0wzp+aGwBWt90gMRoOVf+aWTW10QHI= +golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220308174144-ae0e22291548/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= +gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= +gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.2/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= +gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +helm.sh/helm/v3 v3.8.0/go.mod h1:0nYPSuvuj8TTJDLRSAfbzGGbazPZsayaDpP8s9FfZT8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= +k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= +k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= +k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI= +k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= +k8s.io/apiextensions-apiserver v0.23.1/go.mod h1:0qz4fPaHHsVhRApbtk3MGXNn2Q9M/cVWWhfHdY2SxiM= +k8s.io/apiextensions-apiserver v0.23.4/go.mod h1:TWYAKymJx7nLMxWCgWm2RYGXHrGlVZnxIlGnvtfYu+g= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= +k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= +k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM= +k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= +k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY= +k8s.io/apiserver v0.23.4/go.mod h1:A6l/ZcNtxGfPSqbFDoxxOjEjSKBaQmE+UTveOmMkpNc= +k8s.io/cli-runtime v0.23.1/go.mod h1:r9r8H/qfXo9w+69vwUL7LokKlLRKW5D6A8vUKCx+YL0= +k8s.io/cli-runtime v0.23.4/go.mod h1:7KywUNTUibmHPqmpDFuRO1kc9RhsufHv2lkjCm2YZyM= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= +k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= +k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= +k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA= +k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= +k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= +k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo= +k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E= +k8s.io/component-helpers v0.23.1/go.mod h1:ZK24U+2oXnBPcas2KolLigVVN9g5zOzaHLkHiQMFGr0= +k8s.io/component-helpers v0.23.4/go.mod h1:1Pl7L4zukZ054ElzRbvmZ1FJIU8roBXFOeRFu8zipa4= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.70.0 h1:GMmmjoFOrNepPN0ZeGCzvD2Gh5IKRwdFx8W5PBxVTQU= +k8s.io/klog/v2 v2.70.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 h1:yEQKdMCjzAOvGeiTwG4hO/hNVNtDOuUFvMUZ0OlaIzs= +k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8/go.mod h1:mbJ+NSUoAhuR14N0S63bPkh8MGVSo3VYSGZtH/mfMe0= +k8s.io/kubectl v0.23.1/go.mod h1:Ui7dJKdUludF8yWAOSN7JZEkOuYixX5yF6E6NjoukKE= +k8s.io/kubectl v0.23.4/go.mod h1:Dgb0Rvx/8JKS/C2EuvsNiQc6RZnX0SbHJVG3XUzH6ok= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/metrics v0.23.1/go.mod h1:qXvsM1KANrc+ZZeFwj6Phvf0NLiC+d3RwcsLcdGc+xs= +k8s.io/metrics v0.23.4/go.mod h1:cl6sY9BdVT3DubbpqnkPIKi6mn/F2ltkU4yH1tEJ3Bo= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +oras.land/oras-go v1.1.0/go.mod h1:1A7vR/0KknT2UkJVWh+xMi95I/AhK8ZrxrnUSmXN0bQ= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 h1:2sgAQQcY0dEW2SsQwTXhQV4vO6+rSslYx8K3XmM5hqQ= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= +sigs.k8s.io/kustomize/api v0.11.2/go.mod h1:GZuhith5YcqxIDe0GnRJNx5xxPTjlwaLTt/e+ChUtJA= +sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= +sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= +sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= +sigs.k8s.io/kustomize/kyaml v0.13.3/go.mod h1:/ya3Gk4diiQzlE4mBh7wykyLRFZNvqlbh+JnwQ9Vhrc= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/site-portal/make/frontend/Dockerfile b/site-portal/make/frontend/Dockerfile new file mode 100644 index 00000000..780ea80f --- /dev/null +++ b/site-portal/make/frontend/Dockerfile @@ -0,0 +1,27 @@ +FROM node:15.4.0 as builder + +WORKDIR /build_dir + +COPY . . +RUN cd frontend && npm install && cd .. && make frontend + +FROM photon:4.0 + +RUN tdnf install -y nginx shadow gettext >> /dev/null \ + && tdnf clean all \ + && ln -sf /dev/stdout /var/log/nginx/access.log \ + && ln -sf /dev/stderr /var/log/nginx/error.log + +COPY --from=builder /build_dir/output/frontend /usr/share/nginx/html +COPY --from=builder /build_dir/make/frontend/nginx.conf.template /etc/nginx/conf.d/nginx.conf.template + +RUN groupadd -r -g 10000 nginx && useradd --no-log-init -r -g 10000 -u 10000 nginx \ + && chown -R nginx:nginx /etc/nginx \ + && chown -R nginx:nginx /usr/share/nginx/html + +VOLUME /var/cache/nginx /var/log/nginx /run + +STOPSIGNAL SIGQUIT + +USER nginx +CMD ["/bin/bash", "-c", "SITEPORTAL_SERVER_HOST=${SITEPORTAL_SERVER_HOST:-server} envsubst '${SITEPORTAL_SERVER_HOST}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/nginx.conf && exec nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/site-portal/make/frontend/nginx.conf.template b/site-portal/make/frontend/nginx.conf.template new file mode 100644 index 00000000..a6cde1b0 --- /dev/null +++ b/site-portal/make/frontend/nginx.conf.template @@ -0,0 +1,52 @@ + +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + upstream server { + server ${SITEPORTAL_SERVER_HOST}:8080; + } + + server { + listen 8080; + server_name localhost; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /api/ { + proxy_pass http://server/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_buffering off; + proxy_request_buffering off; + } + } +} \ No newline at end of file diff --git a/site-portal/make/server/Dockerfile b/site-portal/make/server/Dockerfile new file mode 100644 index 00000000..c0d98cff --- /dev/null +++ b/site-portal/make/server/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.17 as builder + +ARG BRANCH +ARG COMMIT + +WORKDIR /workspace + +COPY . . +RUN make server + +FROM photon:4.0 +WORKDIR / +COPY --from=builder /workspace/output . + +RUN tdnf install -y tzdata shadow >> /dev/null \ + && tdnf clean all \ + && groupadd -r -g 10000 portal-server \ + && useradd --no-log-init -r -m -g 10000 -u 10000 portal-server +USER portal-server + +VOLUME /var/lib/site-portal/data/uploaded +ENTRYPOINT ["/site-portal"] diff --git a/site-portal/server/api/auth.go b/site-portal/server/api/auth.go new file mode 100644 index 00000000..b68aacb7 --- /dev/null +++ b/site-portal/server/api/auth.go @@ -0,0 +1,131 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + domainservice "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "k8s.io/apimachinery/pkg/util/rand" +) + +const ( + idKey = "id" + nameKey = "name" + uuidKey = "uuid" + authErrorKey = "AUTH_ERROR" +) + +var authMiddleware *jwt.GinJWTMiddleware + +func getKey() string { + key := viper.GetString("siteportal.jwt.key") + if key == "" { + log.Warn().Msg("no pre-defined jwt key, generating a random one") + key = rand.String(32) + } + return key +} + +// CreateAuthMiddleware creates the authentication middleware +func CreateAuthMiddleware(repo repo.UserRepository) (err error) { + userApp := service.UserApp{UserRepo: repo} + authMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{ + Realm: "site portal jwt", + Key: []byte(getKey()), + Timeout: time.Hour * 24, + MaxRefresh: time.Hour, + IdentityKey: idKey, + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*service.PublicUser); ok { + return jwt.MapClaims{ + idKey: v.ID, + nameKey: v.Name, + uuidKey: v.UUID, + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + return &service.PublicUser{ + ID: uint(claims[idKey].(float64)), + Name: claims[nameKey].(string), + UUID: claims[uuidKey].(string), + } + }, + Authenticator: func(c *gin.Context) (interface{}, error) { + var loginInfo service.LoginInfo + if err := c.ShouldBindJSON(&loginInfo); err != nil { + return "", jwt.ErrMissingLoginValues + } + if user, err := userApp.Login(&loginInfo); err == nil { + log.Info().Msgf("user: %s logged in", loginInfo.Username) + return user, nil + } else if err == domainservice.ErrAccessDenied { + return nil, err + } + return nil, jwt.ErrFailedAuthentication + }, + LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) { + c.JSON(code, GeneralResponse{ + Code: 0, + Message: "", + Data: token, + }) + }, + Authorizator: func(data interface{}, c *gin.Context) bool { + if v, ok := data.(*service.PublicUser); ok && v.Name != "" { + if err := userApp.CheckAccess(v); err != nil { + c.Set(authErrorKey, err) + return false + } + return true + } + return false + }, + Unauthorized: func(c *gin.Context, code int, message string) { + c.JSON(code, GeneralResponse{ + Code: code, + Message: message, + Data: nil, + }) + }, + HTTPStatusMessageFunc: func(e error, c *gin.Context) string { + if e == jwt.ErrForbidden { + if v, exist := c.Get(authErrorKey); exist { + err := v.(error) + return err.Error() + } + } + return e.Error() + }, + TokenLookup: "header: Authorization, cookie: jwt", + TokenHeadName: "Bearer", + SendCookie: true, + SecureCookie: false, + CookieHTTPOnly: true, + CookieName: "jwt", + CookieSameSite: http.SameSiteDefaultMode, + }) + return +} diff --git a/site-portal/server/api/job.go b/site-portal/server/api/job.go new file mode 100644 index 00000000..dbf30ba9 --- /dev/null +++ b/site-portal/server/api/job.go @@ -0,0 +1,503 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "net/http/httputil" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// JobController handles job related APIs +type JobController struct { + jobApp *service.JobApp +} + +// NewJobController returns a controller instance to handle job API requests +func NewJobController(jobRepo repo.JobRepository, + jobParticipantRepo repo.JobParticipantRepository, + projectRepo repo.ProjectRepository, + siteRepo repo.SiteRepository, + projectDataRepo repo.ProjectDataRepository, + modelRepo repo.ModelRepository) *JobController { + return &JobController{ + jobApp: &service.JobApp{ + SiteRepo: siteRepo, + JobRepo: jobRepo, + ParticipantRepo: jobParticipantRepo, + ProjectRepo: projectRepo, + ProjectDataRepo: projectDataRepo, + ModelRepo: modelRepo, + }, + } +} + +// Route set up route mappings to job related APIs +func (controller *JobController) Route(r *gin.RouterGroup) { + job := r.Group("job") + // internal APIs are used by FML manager only + internal := job.Group("internal") + if viper.GetBool("siteportal.tls.enabled") { + internal.Use(certAuthenticator()) + } + { + internal.POST("/create", controller.createRemoteJob) + internal.POST("/:uuid/response", controller.handleJobResponse) + internal.POST("/:uuid/status", controller.handleJobStatusUpdate) + } + + job.Use(authMiddleware.MiddlewareFunc()) + { + job.POST("/:uuid/approve", controller.approveJob) + job.POST("/:uuid/reject", controller.rejectJob) + job.POST("/:uuid/refresh", controller.refreshJob) + job.GET("/:uuid", controller.get) + job.DELETE("/:uuid", controller.delete) + job.POST("/conf/create", controller.generateConf) + job.GET("/predict/participant", controller.getPredictingJobParticipant) + job.GET("/:uuid/data-result/download", controller.downloadDataResult) + job.GET("/components", controller.getJobComponents) + job.POST("/generateDslFromDag", controller.generateDslFromDag) + job.POST("/generateConfFromDag", controller.generateConfFromDag) + } +} + +// approveJob approves the job +// @Summary Approve a pending job +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/approve [post] +func (controller *JobController) approveJob(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + return controller.jobApp.Approve(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// rejectJob rejects the job +// @Summary Disapprove a pending job +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/reject [post] +func (controller *JobController) rejectJob(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + return controller.jobApp.Reject(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// refreshJob retrieve the latest job status +// @Summary Refresh the latest job status +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/refresh [post] +func (controller *JobController) refreshJob(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + return controller.jobApp.Refresh(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of a job +// @Summary Get job's detailed info +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Success 200 {object} GeneralResponse{data=service.JobDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid} [get] +func (controller *JobController) get(c *gin.Context) { + if job, err := func() (*service.JobDetail, error) { + jobUUID := c.Param("uuid") + return controller.jobApp.GetJobDetail(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: job, + } + c.JSON(http.StatusOK, resp) + } +} + +// generateConf returns job configuration and DSL content +// @Summary Get a job's config template, used in json template mode +// @Tags Job +// @Produce json +// @Param request body service.JobSubmissionRequest true "Job requests, not all fields are required: only need to fill related ones according to job type" +// @Success 200 {object} GeneralResponse{data=service.JobConf} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/conf/create [post] +func (controller *JobController) generateConf(c *gin.Context) { + if conf, err := func() (*service.JobConf, error) { + request := &service.JobSubmissionRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return nil, err + } + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + username := claims[nameKey].(string) + if conf, err := controller.jobApp.GenerateConfig(username, request); err != nil { + return nil, err + } else { + return conf, nil + } + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: conf, + } + c.JSON(http.StatusOK, resp) + } +} + +// createRemoteJob creates a new job +// @Summary Create a new job that is created by other site, only called by FML manager +// @Tags Job +// @Produce json +// @Param request body service.RemoteJobCreationRequest true "Job info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/internal/create [post] +func (controller *JobController) createRemoteJob(c *gin.Context) { + if err := func() error { + request := &service.RemoteJobCreationRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return err + } + return controller.jobApp.ProcessNewRemoteJob(request) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleJobResponse process job approval response +// @Summary Handle job response from other sites, only called by FML manager +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Param context body service.JobApprovalContext true "Approval context, containing the sender UUID and approval status" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/internal/{uuid}/response [post] +func (controller *JobController) handleJobResponse(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + approvalContext := &service.JobApprovalContext{} + if err := c.ShouldBindJSON(approvalContext); err != nil { + return err + } + return controller.jobApp.ProcessJobResponse(jobUUID, approvalContext) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleJobStatusUpdate process job status update +// @Summary Handle job status updates, only called by FML manager +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Param context body service.JobStatusUpdateContext true "Job status update context, containing the latest job and participant status" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/internal/{uuid}/status [post] +func (controller *JobController) handleJobStatusUpdate(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + context := &service.JobStatusUpdateContext{} + if err := c.ShouldBindJSON(context); err != nil { + return err + } + return controller.jobApp.ProcessJobStatusUpdate(jobUUID, context) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getPredictingJobParticipant returns participant list for creating a predicting job from a model +// @Summary Get allowed participants for a predicting job from a model +// @Tags Job +// @Produce json +// @Param modelUUID query string true "UUID of a trained model" +// @Success 200 {object} GeneralResponse{data=[]service.JobParticipantInfoBase} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/predict/participant [get] +func (controller *JobController) getPredictingJobParticipant(c *gin.Context) { + if data, err := func() ([]service.JobParticipantInfoBase, error) { + modelUUID := c.Query("modelUUID") + return controller.jobApp.GeneratePredictingJobParticipants(modelUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// downloadDataResult returns predict/PSI job result data +// @Summary the result data of a Predicting or PSI job, XXX: currently it will return an error message due to a bug in FATE +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid}/data-result/download [get] +func (controller *JobController) downloadDataResult(c *gin.Context) { + if req, err := func() (*http.Request, error) { + jobUUID := c.Param("uuid") + return controller.jobApp.GetDataResultDownloadRequest(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + proxy := &httputil.ReverseProxy{ + Director: func(*http.Request) { + // no-op as we don't need to change the request + }, + } + proxy.ServeHTTP(c.Writer, req) + } +} + +// delete a job +// @Summary Delete the job. The job will be marked as delete in this site, but still viewable in other sites +// @Tags Job +// @Produce json +// @Param uuid path string true "Job UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/{uuid} [delete] +func (controller *JobController) delete(c *gin.Context) { + if err := func() error { + jobUUID := c.Param("uuid") + return controller.jobApp.DeleteJob(jobUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getJobComponents returns all the components for a model and their default configs +// @Summary Get all the components and their default configs. The returned format is json +// @Tags Job +// @Produce json +// @Success 200 {object} GeneralResponse{data=string} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/components [get] +func (controller *JobController) getJobComponents(c *gin.Context) { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: controller.jobApp.LoadJobComponents(), + } + c.JSON(http.StatusOK, resp) +} + +// generateDslFromDag returns the DSL json file from the DAG the user draw +// @Summary Generate the DSL json file from the DAG the user draw, should be called by UI only +// @Tags Job +// @Produce json +// @Param rawJson body service.JobRawDagJson true "The raw json, the value should be a serialized json string" +// @Success 200 {object} GeneralResponse{data=string} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/generateDslFromDag [post] +func (controller *JobController) generateDslFromDag(c *gin.Context) { + if rawJson, err := func() (string, error) { + request := &service.JobRawDagJson{} + if err := c.ShouldBindJSON(request); err != nil { + return "", err + } + return request.RawJson, nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + res, err := controller.jobApp.GenerateDslFromDag(rawJson) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: res, + } + c.JSON(http.StatusOK, resp) + } + } +} + +// generateConfFromDag returns the conf json file from the DAG the user draw +// @Summary Generate the conf json file from the DAG the user draw, the conf file can be consumed by Fateflow +// @Tags Job +// @Produce json +// @Param generateJobConfRequest body service.GenerateJobConfRequest true "The request for generate the conf json file" +// @Success 200 {object} GeneralResponse{data=string} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /job/generateConfFromDag [post] +func (controller *JobController) generateConfFromDag(c *gin.Context) { + if generateJobConfRequest, err := func() (*service.GenerateJobConfRequest, error) { + request := &service.GenerateJobConfRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return nil, err + } + return request, nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + claims := jwt.ExtractClaims(c) + username := claims[nameKey].(string) + res, err := controller.jobApp.GenerateConfFromDag(username, generateJobConfRequest) + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: res, + } + c.JSON(http.StatusOK, resp) + } + } +} diff --git a/site-portal/server/api/local_data.go b/site-portal/server/api/local_data.go new file mode 100644 index 00000000..74762e84 --- /dev/null +++ b/site-portal/server/api/local_data.go @@ -0,0 +1,256 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "path/filepath" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/gin-gonic/gin" +) + +// LocalDataController handles local data related APIs +type LocalDataController struct { + localDataApp *service.LocalDataApp +} + +// NewLocalDataController returns a controller instance to handle local data API requests +func NewLocalDataController(localDataRepo repo.LocalDataRepository, + siteRepo repo.SiteRepository, + projectRepo repo.ProjectRepository, + projectDataRepo repo.ProjectDataRepository) *LocalDataController { + return &LocalDataController{ + localDataApp: &service.LocalDataApp{ + LocalDataRepo: localDataRepo, + SiteRepo: siteRepo, + ProjectRepo: projectRepo, + ProjectDataRepo: projectDataRepo, + }, + } +} + +// Route set up route mappings to local data related APIs +func (controller *LocalDataController) Route(r *gin.RouterGroup) { + data := r.Group("data") + data.Use(authMiddleware.MiddlewareFunc()) + { + data.POST("", controller.upload) + data.GET("", controller.list) + data.GET("/:uuid", controller.get) + data.GET("/:uuid/columns", controller.getColumns) + data.GET("/:uuid/file", controller.download) + data.DELETE("/:uuid", controller.delete) + data.PUT("/:uuid/idmetainfo", controller.putIdMetaInfo) + } +} + +// upload uploads a local csv data +// @Summary Upload a local csv data +// @Tags LocalData +// @Produce json +// @Param file formData file true "The csv file" +// @Param name formData string true "Data name" +// @Param description formData string true "Data description" +// @Success 200 {object} GeneralResponse{} "Success, the data field is the data UUID" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data [post] +func (controller *LocalDataController) upload(c *gin.Context) { + if uuid, err := func() (string, error) { + f, err := c.FormFile("file") + if err != nil { + return "", err + } + uploadRequest := service.LocalDataUploadRequest{} + if err = c.ShouldBind(&uploadRequest); err != nil { + return "", err + } + uploadRequest.FileHeader = f + return controller.localDataApp.Upload(&uploadRequest) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: uuid, + } + c.JSON(http.StatusOK, resp) + } +} + +// list returns all data records +// @Summary List all data records +// @Tags LocalData +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.LocalDataListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data [get] +func (controller *LocalDataController) list(c *gin.Context) { + if dataList, err := controller.localDataApp.List(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: dataList, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of a data record +// @Summary Get data record's detailed info +// @Tags LocalData +// @Produce json +// @Param uuid path string true "Data UUID" +// @Success 200 {object} GeneralResponse{data=service.LocalDataDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data/{uuid} [get] +func (controller *LocalDataController) get(c *gin.Context) { + uuid := c.Param("uuid") + if dataDetail, err := controller.localDataApp.Get(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: dataDetail, + } + c.JSON(http.StatusOK, resp) + } +} + +// download returns the original csv data file +// @Summary Download data file +// @Tags LocalData +// @Produce json +// @Param uuid path string true "Data UUID" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data/{uuid}/file [get] +func (controller *LocalDataController) download(c *gin.Context) { + uuid := c.Param("uuid") + if path, err := controller.localDataApp.GetFilePath(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + // TODO: investigate and implement "chunked Transfer-Encoding" + c.FileAttachment(path, filepath.Base(path)) + } +} + +// delete removes the data +// @Summary Delete the data file, both the local copy and the FATE table +// @Tags LocalData +// @Produce json +// @Param uuid path string true "Data UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data/{uuid} [delete] +func (controller *LocalDataController) delete(c *gin.Context) { + uuid := c.Param("uuid") + if err := controller.localDataApp.DeleteData(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// putIdMetaInfo update data record ID meta info +// @Summary Update data record's ID meta info +// @Tags LocalData +// @Produce json +// @Param info body service.LocalDataIDMetaInfoUpdateRequest true "The meta info" +// @Param uuid path string true "Data UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data/{uuid}/idmetainfo [put] +func (controller *LocalDataController) putIdMetaInfo(c *gin.Context) { + if err := func() error { + uuid := c.Param("uuid") + info := &service.LocalDataIDMetaInfoUpdateRequest{} + if err := c.ShouldBindJSON(info); err != nil { + return err + } + if err := controller.localDataApp.UpdateIDMetaInfo(uuid, info); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// getColumns returns a list of the data's headers +// @Summary Get data headers +// @Tags LocalData +// @Produce json +// @Param uuid path string true "Data UUID" +// @Success 200 {object} GeneralResponse{data=[]string} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /data/{uuid}/columns [get] +func (controller *LocalDataController) getColumns(c *gin.Context) { + uuid := c.Param("uuid") + if columns, err := controller.localDataApp.GetColumns(uuid); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: columns, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/site-portal/server/api/model.go b/site-portal/server/api/model.go new file mode 100644 index 00000000..bf53a451 --- /dev/null +++ b/site-portal/server/api/model.go @@ -0,0 +1,238 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + domainService "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// ModelController handles model related APIs +type ModelController struct { + modelApp *service.ModelApp +} + +// NewModelController returns a controller instance to handle model API requests +func NewModelController(modelRepo repo.ModelRepository, + modelDeploymentRepo repo.ModelDeploymentRepository, + siteRepo repo.SiteRepository, + projectRepo repo.ProjectRepository) *ModelController { + return &ModelController{ + modelApp: &service.ModelApp{ + ModelRepo: modelRepo, + ModelDeploymentRepo: modelDeploymentRepo, + SiteRepo: siteRepo, + ProjectRepo: projectRepo, + }, + } +} + +// Route set up route mappings to model related APIs +func (controller *ModelController) Route(r *gin.RouterGroup) { + model := r.Group("model") + internal := model.Group("internal") + if viper.GetBool("siteportal.tls.enabled") { + internal.Use(certAuthenticator()) + } + { + internal.POST("/event/create", controller.create) + } + model.Use(authMiddleware.MiddlewareFunc()) + { + model.GET("", controller.list) + model.GET("/:uuid", controller.get) + model.POST("/:uuid/publish", controller.deployModel) + model.GET("/:uuid/supportedDeploymentTypes", controller.getSupportedDeployments) + model.DELETE("/:uuid", controller.delete) + } +} + +// list returns a list of models +// @Summary Get model list +// @Tags Model +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model [get] +func (controller *ModelController) list(c *gin.Context) { + if data, err := func() ([]service.ModelListItem, error) { + return controller.modelApp.List("") + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of a model +// @Summary Get model's detailed info +// @Tags Model +// @Produce json +// @Param uuid path string true "Model UUID" +// @Success 200 {object} GeneralResponse{data=service.ModelDetail} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model/{uuid} [get] +func (controller *ModelController) get(c *gin.Context) { + if data, err := func() (*service.ModelDetail, error) { + modelUUID := c.Param("uuid") + return controller.modelApp.Get(modelUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// delete deletes the specified model +// @Summary Delete the model +// @Tags Model +// @Produce json +// @Param uuid path string true "Model UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model/{uuid} [delete] +func (controller *ModelController) delete(c *gin.Context) { + modelUUID := c.Param("uuid") + if err := controller.modelApp.Delete(modelUUID); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// create process model creation event +// @Summary Handle model creation event, called by the job context only +// @Tags Model +// @Produce json +// @Param request body service.ModelCreationRequest true "Creation Request" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model/internal/event/create [post] +func (controller *ModelController) create(c *gin.Context) { + if err := func() error { + request := &service.ModelCreationRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return err + } + return controller.modelApp.Create(request) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// deployModel publish the model to online serving system +// @Summary Publish model to online serving system +// @Tags Model +// @Produce json +// @Param uuid path string true "Model UUID" +// @Param request body service.ModelDeploymentRequest true "Creation Request" +// @Success 200 {object} GeneralResponse{data=entity.ModelDeployment} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model/{uuid}/publish [post] +func (controller *ModelController) deployModel(c *gin.Context) { + if deployment, err := func() (*entity.ModelDeployment, error) { + modelUUID := c.Param("uuid") + request := &domainService.ModelDeploymentRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return nil, err + } + request.ModelUUID = modelUUID + return controller.modelApp.Publish(request) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: deployment, + } + c.JSON(http.StatusOK, resp) + } +} + +// getSupportedDeployments returns list of the deployment type this model can use +// @Summary Get list of deployment types (KFServing, FATE-Serving, etc.) this model can use +// @Tags Model +// @Produce json +// @Param uuid path string true "Model UUID" +// @Success 200 {object} GeneralResponse{data=[]entity.ModelDeploymentType} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /model/{uuid}/supportedDeploymentTypes [get] +func (controller *ModelController) getSupportedDeployments(c *gin.Context) { + if types, err := func() ([]entity.ModelDeploymentType, error) { + modelUUID := c.Param("uuid") + return controller.modelApp.GetSupportedDeploymentTypes(modelUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: types, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/site-portal/server/api/project.go b/site-portal/server/api/project.go new file mode 100644 index 00000000..3a0f9d5d --- /dev/null +++ b/site-portal/server/api/project.go @@ -0,0 +1,1088 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + domainService "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// ProjectController handles project related APIs +type ProjectController struct { + projectApp *service.ProjectApp + jobApp *service.JobApp + modelApp *service.ModelApp +} + +// NewProjectController returns a controller instance to handle project API requests +func NewProjectController(projectRepo repo.ProjectRepository, + siteRepo repo.SiteRepository, + participantRepo repo.ProjectParticipantRepository, + invitationRepo repo.ProjectInvitationRepository, + projectDataRepo repo.ProjectDataRepository, + localDataRepo repo.LocalDataRepository, + jobRepo repo.JobRepository, + jobParticipantRepo repo.JobParticipantRepository, + modelRepo repo.ModelRepository) *ProjectController { + + jobApp := &service.JobApp{ + SiteRepo: siteRepo, + JobRepo: jobRepo, + ParticipantRepo: jobParticipantRepo, + ProjectRepo: projectRepo, + ProjectDataRepo: projectDataRepo, + ModelRepo: modelRepo, + } + return &ProjectController{ + projectApp: &service.ProjectApp{ + ProjectRepo: projectRepo, + ParticipantRepo: participantRepo, + SiteRepo: siteRepo, + InvitationRepo: invitationRepo, + ProjectDataRepo: projectDataRepo, + LocalDataRepo: localDataRepo, + JobApp: jobApp, + ProjectSyncService: domainService.NewProjectSyncService(), + }, + jobApp: jobApp, + modelApp: &service.ModelApp{ + ModelRepo: modelRepo, + ProjectRepo: projectRepo, + }, + } +} + +// Route set up route mappings to project related APIs +func (controller *ProjectController) Route(r *gin.RouterGroup) { + project := r.Group("project") + internal := project.Group("internal") + if viper.GetBool("siteportal.tls.enabled") { + internal.Use(certAuthenticator()) + } + { + internal.POST("/:uuid/close", controller.handleProjectClosing) + + internal.POST("/invitation", controller.handleInvitation) + internal.POST("/invitation/:uuid/accept", controller.handleInvitationAcceptance) + internal.POST("/invitation/:uuid/reject", controller.handleInvitationRejection) + internal.POST("/invitation/:uuid/revoke", controller.handleInvitationRevocation) + + internal.POST("/:uuid/participants", controller.createParticipants) + internal.POST("/:uuid/participant/:siteUUID/dismiss", controller.handleParticipantDismissal) + internal.POST("/:uuid/participant/:siteUUID/leave", controller.handleParticipantLeaving) + + internal.POST("/event/participant/update", controller.handleParticipantInfoUpdate) + internal.POST("/event/participant/sync", controller.handleParticipantSync) + internal.POST("/event/data/sync", controller.handleDataSync) + internal.POST("/event/list/sync", controller.handleProjectListSync) + + internal.POST("/:uuid/data/associate", controller.handleRemoteDataAssociation) + internal.POST("/:uuid/data/dismiss", controller.handleRemoteDataDismissal) + } + project.Use(authMiddleware.MiddlewareFunc()) + { + project.POST("", controller.create) + project.GET("", controller.list) + project.GET("/:uuid", controller.get) + project.GET("/:uuid/participant", controller.listParticipants) + project.DELETE("/:uuid/participant/:participantUUID", controller.removeParticipant) + + project.POST("/:uuid/invitation", controller.inviteParticipant) + project.PUT("/:uuid/autoapprovalstatus", controller.toggleAutoApproval) + project.POST("/:uuid/join", controller.joinProject) + project.POST("/:uuid/reject", controller.rejectProject) + project.POST("/:uuid/leave", controller.leaveProject) + project.POST("/:uuid/close", controller.closeProject) + + project.POST("/:uuid/data", controller.addData) + project.GET("/:uuid/data", controller.listData) + project.GET("/:uuid/data/local", controller.listLocalAvailableData) + project.DELETE("/:uuid/data/:dataUUID", controller.removeData) + + project.GET("/:uuid/job", controller.listJob) + project.POST("/:uuid/job", controller.submitJob) + + project.GET("/:uuid/model", controller.listModel) + } +} + +// create Create a new local project +// @Summary Create a new project +// @Tags Project +// @Produce json +// @Param project body service.ProjectCreationRequest true "Basic project info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project [post] +func (controller *ProjectController) create(c *gin.Context) { + if err := func() error { + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + username := claims[nameKey].(string) + request := &service.ProjectCreationRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return err + } + if err := controller.projectApp.CreateLocalProject(request, username); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// list returns all projects +// @Summary List all project +// @Tags Project +// @Produce json +// @Success 200 {object} GeneralResponse{data=service.ProjectList} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project [get] +func (controller *ProjectController) list(c *gin.Context) { + if projectList, err := controller.projectApp.List(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: projectList, + } + c.JSON(http.StatusOK, resp) + } +} + +// listParticipants returns all participants +// @Summary List participants +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param all query bool false "if set to true, returns all sites, including not joined ones" +// @Success 200 {object} GeneralResponse{data=[]service.ProjectParticipant} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/participant [get] +func (controller *ProjectController) listParticipants(c *gin.Context) { + queryAll, err := strconv.ParseBool(c.DefaultQuery("all", "false")) + if err != nil { + queryAll = false + } + projectUUID := c.Param("uuid") + if participantList, err := controller.projectApp.ListParticipant(projectUUID, queryAll); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: participantList, + } + c.JSON(http.StatusOK, resp) + } +} + +// inviteParticipant sends project invitation to other projects +// @Summary Invite other site to this project +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param info body service.ProjectParticipantBase true "target site information" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/invitation [post] +func (controller *ProjectController) inviteParticipant(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + targetSite := &service.ProjectParticipantBase{} + if err := c.ShouldBindJSON(targetSite); err != nil { + return err + } + return controller.projectApp.InviteParticipant(projectUUID, targetSite) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitation process a project invitation +// @Summary Process project invitation, called by FML manager only +// @Tags Project +// @Produce json +// @Param invitation body service.ProjectInvitationRequest true "invitation request" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/invitation [post] +func (controller *ProjectController) handleInvitation(c *gin.Context) { + if err := func() error { + invitationRequest := &service.ProjectInvitationRequest{} + if err := c.ShouldBindJSON(invitationRequest); err != nil { + return err + } + return controller.projectApp.ProcessInvitation(invitationRequest) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// toggleAutoApproval changes project auto-approval status +// @Summary Change a project's auto-approval status +// @Tags Project +// @Produce json +// @Param status body service.ProjectAutoApprovalStatus true "The auto-approval status, only an 'enabled' field" +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/autoapprovalstatus [put] +func (controller *ProjectController) toggleAutoApproval(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + status := &service.ProjectAutoApprovalStatus{} + if err := c.ShouldBindJSON(status); err != nil { + return err + } + return controller.projectApp.ToggleAutoApprovalStatus(projectUUID, status) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// get returns detailed information of a project +// @Summary Get project's detailed info +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=service.ProjectInfo} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid} [get] +func (controller *ProjectController) get(c *gin.Context) { + if project, err := func() (*service.ProjectInfo, error) { + projectUUID := c.Param("uuid") + return controller.projectApp.GetProject(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: project, + } + c.JSON(http.StatusOK, resp) + } +} + +// joinProject joins a project +// @Summary Join a pending/invited project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/join [post] +func (controller *ProjectController) joinProject(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.JoinOrRejectProject(projectUUID, true) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// rejectProject joins a project +// @Summary Reject to join a pending/invited project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/reject [post] +func (controller *ProjectController) rejectProject(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.JoinOrRejectProject(projectUUID, false) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// leaveProject leave the specified project +// @Summary Leave the joined project created by other site +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/leave [post] +func (controller *ProjectController) leaveProject(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.LeaveProject(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// closeProject close the specified project +// @Summary Close the managed project, only available to project managing site +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/close [post] +func (controller *ProjectController) closeProject(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.CloseProject(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationAcceptance process a project invitation acceptance +// @Summary Process invitation acceptance response, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/invitation/{uuid}/accept [post] +func (controller *ProjectController) handleInvitationAcceptance(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationResponse(invitationUUID, true) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationRejection process a project invitation rejection +// @Summary Process invitation rejection response, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/invitation/{uuid}/reject [post] +func (controller *ProjectController) handleInvitationRejection(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationResponse(invitationUUID, false) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleInvitationRevocation process a project invitation revocation +// @Summary Process invitation revocation request, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Invitation UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/invitation/{uuid}/revoke [post] +func (controller *ProjectController) handleInvitationRevocation(c *gin.Context) { + if err := func() error { + invitationUUID := c.Param("uuid") + return controller.projectApp.ProcessInvitationRevocation(invitationUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantLeaving process a project participant removal event +// @Summary Process participant leaving request, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param siteUUID path string true "Site UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/participant/{siteUUID}/leave [post] +func (controller *ProjectController) handleParticipantLeaving(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + siteUUID := c.Param("siteUUID") + return controller.projectApp.ProcessParticipantLeaving(projectUUID, siteUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantDismissal process a project participant dismissal event +// @Summary Process participant dismissal event, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param siteUUID path string true "Site UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/participant/{siteUUID}/dismiss [post] +func (controller *ProjectController) handleParticipantDismissal(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + siteUUID := c.Param("siteUUID") + return controller.projectApp.ProcessParticipantDismissal(projectUUID, siteUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// createParticipants receive participants info from FML manager +// @Summary Create joined participants for a project, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param participantList body []entity.ProjectParticipant true "participants list" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/participants [post] +func (controller *ProjectController) createParticipants(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + var participants []entity.ProjectParticipant + if err := c.ShouldBindJSON(&participants); err != nil { + return err + } + return controller.projectApp.CreateRemoteProjectParticipants(projectUUID, participants) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// removeParticipant remove pending or joined participant +// @Summary Remove pending participant (revoke invitation) or dismiss joined participant +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param participantUUID path string true "Participant UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/participant/{participantUUID} [delete] +func (controller *ProjectController) removeParticipant(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + siteUUID := c.Param("participantUUID") + return controller.projectApp.RemoveProjectParticipants(projectUUID, siteUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantInfoUpdate process a participant info update event +// @Summary Process participant info update event, called by the FML manager only +// @Tags Project +// @Produce json +// @Param participant body service.ProjectParticipantBase true "Updated participant info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/event/participant/update [post] +func (controller *ProjectController) handleParticipantInfoUpdate(c *gin.Context) { + if err := func() error { + var participant service.ProjectParticipantBase + if err := c.ShouldBindJSON(&participant); err != nil { + return err + } + return controller.projectApp.ProcessParticipantInfoUpdate(&participant) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// listLocalAvailableData returns a list of local data that can be associated to current project +// @Summary Get available local data for this project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data/local [get] +func (controller *ProjectController) listLocalAvailableData(c *gin.Context) { + if data, err := func() ([]service.ProjectData, error) { + projectUUID := c.Param("uuid") + return controller.projectApp.ListLocalData(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// listData returns a list of associated data of current project +// @Summary Get associated data list for this project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param participant query string false "participant uuid, i.e. the providing site uuid; if set, only returns the associated data of the specified participant" +// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data [get] +func (controller *ProjectController) listData(c *gin.Context) { + if data, err := func() ([]service.ProjectData, error) { + projectUUID := c.Param("uuid") + participantUUID := c.DefaultQuery("participant", "") + return controller.projectApp.ListData(projectUUID, participantUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// addData associate local data +// @Summary Associate local data to current project +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param request body service.ProjectDataAssociationRequest true "Local data info" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data [post] +func (controller *ProjectController) addData(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + request := &service.ProjectDataAssociationRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return err + } + if err := controller.projectApp.CreateDataAssociation(projectUUID, request); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// removeData dismiss data association +// @Summary Remove associated data from the current project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Param dataUUID path string true "Data UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/data/{dataUUID} [delete] +func (controller *ProjectController) removeData(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + dataUUID := c.Param("dataUUID") + if err := controller.projectApp.RemoveDataAssociation(projectUUID, dataUUID); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleRemoteDataAssociation associate remote data +// @Summary Add associated remote data to current project, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param data body []entity.ProjectData true "Remote data information" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/data/associate [post] +func (controller *ProjectController) handleRemoteDataAssociation(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + var request []entity.ProjectData + if err := c.ShouldBindJSON(&request); err != nil { + return err + } + if err := controller.projectApp.CreateRemoteProjectDataAssociation(projectUUID, request); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleRemoteDataDismissal dismiss remote data +// @Summary Dismiss associated remote data from current project, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param data body []string true "Data UUID list" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/data/dismiss [post] +func (controller *ProjectController) handleRemoteDataDismissal(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + var request []string + if err := c.ShouldBindJSON(&request); err != nil { + return err + } + if err := controller.projectApp.DismissRemoteProjectDataAssociation(projectUUID, request); err != nil { + return err + } + return nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// listJob returns a list of jobs in the current project +// @Summary Get job list for this project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=[]service.JobListItemBase} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/job [get] +func (controller *ProjectController) listJob(c *gin.Context) { + if data, err := func() ([]service.JobListItemBase, error) { + projectUUID := c.Param("uuid") + return controller.jobApp.List(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// submitJob create new job +// @Summary Create a new job +// @Tags Project +// @Produce json +// @Param uuid path string true "project UUID" +// @Param request body service.JobSubmissionRequest true "Job requests, only fill related field according to job type" +// @Success 200 {object} GeneralResponse{data=service.JobListItemBase} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/job [post] +func (controller *ProjectController) submitJob(c *gin.Context) { + if job, err := func() (*service.JobListItemBase, error) { + projectUUID := c.Param("uuid") + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + username := claims[nameKey].(string) + request := &service.JobSubmissionRequest{} + if err := c.ShouldBindJSON(request); err != nil { + return nil, err + } + //log.Info().Msg(fmt.Sprint(request)) + + // project status check + if err := controller.projectApp.EnsureProjectIsOpen(projectUUID); err != nil { + return nil, err + } + + request.ProjectUUID = projectUUID + job, err := controller.jobApp.SubmitJob(username, request) + if err != nil { + return nil, err + } + return job, err + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: job, + } + c.JSON(http.StatusOK, resp) + } +} + +// listModel returns a list of models in the current project +// @Summary Get model list for this project +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/{uuid}/model [get] +func (controller *ProjectController) listModel(c *gin.Context) { + if data, err := func() ([]service.ModelListItem, error) { + projectUUID := c.Param("uuid") + return controller.modelApp.List(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleProjectClosing process a project closing event +// @Summary Process project closing event, called by FML manager only +// @Tags Project +// @Produce json +// @Param uuid path string true "Project UUID" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/{uuid}/close [post] +func (controller *ProjectController) handleProjectClosing(c *gin.Context) { + if err := func() error { + projectUUID := c.Param("uuid") + return controller.projectApp.ProcessProjectClosing(projectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleParticipantSync process a participant info sync event +// @Summary Process participant info sync event, to sync the participant info from fml manager +// @Tags Project +// @Produce json +// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/event/participant/sync [post] +func (controller *ProjectController) handleParticipantSync(c *gin.Context) { + if err := func() error { + var event service.ProjectResourceSyncRequest + if err := c.ShouldBindJSON(&event); err != nil { + return err + } + return controller.projectApp.SyncProjectParticipant(event.ProjectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleDataSync process a data association sync event +// @Summary Process data sync event, to sync the data association info from fml manager +// @Tags Project +// @Produce json +// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced" +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/event/data/sync [post] +func (controller *ProjectController) handleDataSync(c *gin.Context) { + if err := func() error { + var event service.ProjectResourceSyncRequest + if err := c.ShouldBindJSON(&event); err != nil { + return err + } + return controller.projectApp.SyncProjectData(event.ProjectUUID) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// handleProjectListSync process a project list sync event +// @Summary Process list sync event, to sync the projects list status from fml manager +// @Tags Project +// @Produce json +// @Success 200 {object} GeneralResponse{} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /project/internal/event/list/sync [post] +func (controller *ProjectController) handleProjectListSync(c *gin.Context) { + if err := controller.projectApp.SyncProject(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/site-portal/server/api/site.go b/site-portal/server/api/site.go new file mode 100644 index 00000000..0b1861b7 --- /dev/null +++ b/site-portal/server/api/site.go @@ -0,0 +1,200 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/gin-gonic/gin" +) + +// SiteController provides API handlers for the site related APIs +type SiteController struct { + siteAppService *service.SiteApp +} + +// NewSiteController returns a controller instance to handle site API requests +func NewSiteController(repo repo.SiteRepository) *SiteController { + return &SiteController{ + siteAppService: &service.SiteApp{ + SiteRepo: repo, + }, + } +} + +// Route set up route mappings to site related APIs +func (controller *SiteController) Route(r *gin.RouterGroup) { + site := r.Group("site") + site.Use(authMiddleware.MiddlewareFunc()) + { + site.GET("", controller.getSite) + site.PUT("", controller.putSite) + site.POST("/fateflow/connect", controller.connectFATEFlow) + site.POST("/kubeflow/connect", controller.connectKubeflow) + site.POST("/fmlmanager/connect", controller.connectFMLManager) + } +} + +// getSite returns the site data +// @Summary Return site data +// @Tags Site +// @Produce json +// @Success 200 {object} GeneralResponse{data=entity.Site} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site [get] +func (controller *SiteController) getSite(c *gin.Context) { + site, err := controller.siteAppService.GetSite() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Message: "", + Data: site, + } + c.JSON(http.StatusOK, resp) + } +} + +// putSite update site related information +// @Summary Update site information +// @Tags Site +// @Produce json +// @Param site body entity.Site true "The site information, some info like id, UUID, connected status cannot be changed" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site [put] +func (controller *SiteController) putSite(c *gin.Context) { + if err := func() error { + updatedSiteInfo := &entity.Site{} + if err := c.ShouldBindJSON(updatedSiteInfo); err != nil { + return err + } + return controller.siteAppService.UpdateSite(updatedSiteInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// connectFATEFlow test connection to fate flow +// @Summary Test site connection to fate flow service +// @Tags Site +// @Produce json +// @Param connectInfo body service.FATEFlowConnectionInfo true "The fate flow connection info" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site/fateflow/connect [post] +func (controller *SiteController) connectFATEFlow(c *gin.Context) { + if err := func() error { + connectionInfo := &service.FATEFlowConnectionInfo{} + if err := c.ShouldBindJSON(connectionInfo); err != nil { + return err + } + return controller.siteAppService.TestFATEFlowConnection(connectionInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// connectKubeflow test connection to Kubeflow +// @Summary Test site connection to Kubeflow, including MinIO and KFServing +// @Tags Site +// @Produce json +// @Param config body valueobject.KubeflowConfig true "The Kubeflow config info" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site/kubeflow/connect [post] +func (controller *SiteController) connectKubeflow(c *gin.Context) { + if err := func() error { + connectionInfo := &valueobject.KubeflowConfig{} + if err := c.ShouldBindJSON(connectionInfo); err != nil { + return err + } + return controller.siteAppService.TestKubeflowConnection(connectionInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} + +// connectFMLManager registers the current site to fml manager +// @Summary Connect to fml manager and register itself +// @Tags Site +// @Produce json +// @Param connectInfo body service.FMLManagerConnectionInfo true "The FML Manager endpoint" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site/fmlmanager/connect [post] +func (controller *SiteController) connectFMLManager(c *gin.Context) { + if err := func() error { + connectionInfo := &service.FMLManagerConnectionInfo{} + if err := c.ShouldBindJSON(connectionInfo); err != nil { + return err + } + return controller.siteAppService.RegisterToFMLManager(connectionInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/site-portal/server/api/tls_auth.go b/site-portal/server/api/tls_auth.go new file mode 100644 index 00000000..f69f9872 --- /dev/null +++ b/site-portal/server/api/tls_auth.go @@ -0,0 +1,38 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +// certAuthenticator is used to further check the caller's certificate +func certAuthenticator() gin.HandlerFunc { + return func(c *gin.Context) { + + clientCommonName := c.GetHeader("X-SP-CLIENT-SDN") + clientVerify := c.GetHeader("X-SP-CLIENT-VERIFY") + // clientCert := c.GetHeader("X-SP-CLIENT-CERT") + + log.Info().Msgf("Request URL: %s", c.Request.URL.String()) + log.Info().Msgf("Client common name in X-SP-CLIENT-SDN is: %s", clientCommonName) + log.Info().Msgf("Client verify result in X-SP-CLIENT-VERIFY is: %s", clientVerify) + + // TODO: validate the clientCommonName, like if its domain is same to the sitePortalCommonName's domain + + c.Next() + } +} diff --git a/site-portal/server/api/types.go b/site-portal/server/api/types.go new file mode 100644 index 00000000..c59fa812 --- /dev/null +++ b/site-portal/server/api/types.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// GeneralResponse is used for typical API response +type GeneralResponse struct { + Code int `json:"code" example:"0"` + Message string `json:"message" example:"success"` + Data interface{} `json:"data" swaggertype:"object"` +} diff --git a/site-portal/server/api/user.go b/site-portal/server/api/user.go new file mode 100644 index 00000000..9111bb09 --- /dev/null +++ b/site-portal/server/api/user.go @@ -0,0 +1,220 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strconv" + + "github.com/FederatedAI/FedLCM/site-portal/server/application/service" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// UserController manages user related API calls +type UserController struct { + userAppService *service.UserApp +} + +// NewUserController returns a controller instance to handle user API requests +func NewUserController(repo repo.UserRepository) *UserController { + return &UserController{ + userAppService: &service.UserApp{ + UserRepo: repo, + }, + } +} + +// Route set up route mappings to user related APIs +func (controller *UserController) Route(r *gin.RouterGroup) { + users := r.Group("user") + { + users.POST("/login", controller.login) + users.POST("/logout", controller.logout) + } + users.Use(authMiddleware.MiddlewareFunc()) + { + users.GET("", controller.listUsers) + users.GET("/current", controller.getCurrentUsername) + users.PUT("/:id/permission", controller.updatePermission) + users.PUT("/:id/password", controller.updatePassword) + } +} + +// listUsers list all users +// @Summary List all saved users +// @Tags User +// @Produce json +// @Success 200 {object} GeneralResponse{data=[]service.PublicUser} "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user [get] +func (controller *UserController) listUsers(c *gin.Context) { + users, err := controller.userAppService.GetUsers() + if err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: users, + } + c.JSON(http.StatusOK, resp) + } +} + +// updatePermission update user permission +// @Summary Update user permission +// @Tags User +// @Produce json +// @Param permission body valueobject.UserPermissionInfo true "Permission, must contain all permissions, otherwise the missing once will be considered as false" +// @Param id path string true "User ID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/{id}/permission [put] +func (controller *UserController) updatePermission(c *gin.Context) { + if data, err := func() (interface{}, error) { + userId, err := strconv.Atoi(c.Param("id")) + if err != nil { + return nil, err + } + user := &service.PublicUser{ + ID: uint(userId), + } + if err := c.ShouldBindJSON(&user.UserPermissionInfo); err != nil { + return nil, err + } + claims := jwt.ExtractClaims(c) + // XXX: enhance this simple check + username := claims[nameKey].(string) + if username != "Admin" { + return nil, errors.New("only Admin user can change permissions") + } + return nil, controller.userAppService.UpdateUserPermission(user) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + Data: nil, + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: data, + } + c.JSON(http.StatusOK, resp) + } +} + +// login login to site portal using the provided credentials +// @Summary login to site portal +// @Tags User +// @Produce json +// @Param credentials body service.LoginInfo true "credentials for login" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Router /user/login [post] +func (controller *UserController) login(c *gin.Context) { + authMiddleware.LoginHandler(c) +} + +// logout logout from the site portal +// @Summary logout from the site portal +// @Tags User +// @Produce json +// @Success 200 {object} GeneralResponse "Success" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/logout [post] +func (controller *UserController) logout(c *gin.Context) { + authMiddleware.LogoutHandler(c) +} + +// getCurrentUser return current user +// @Summary Return current user in the jwt token +// @Tags User +// @Produce json +// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/current [get] +func (controller *UserController) getCurrentUsername(c *gin.Context) { + if username, err := func() (string, error) { + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + username := claims[nameKey].(string) + return username, nil + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + Data: username, + } + c.JSON(http.StatusOK, resp) + } +} + +// updatePassword update user password +// @Summary Update user Password +// @Tags User +// @Produce json +// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 401 {object} GeneralResponse "Unauthorized operation" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /user/{id}/password [put] +func (controller *UserController) updatePassword(c *gin.Context) { + if err := func() error { + userId, err := strconv.Atoi(c.Param("id")) + if err != nil { + return err + } + claims := jwt.ExtractClaims(c) + // the auth middleware makes sure username exists + currentId := int(claims[idKey].(float64)) + if userId != currentId { + return errors.New("invalid user id") + } + passwordChangeInfo := &service.PwdChangeInfo{} + if err := c.ShouldBindJSON(&passwordChangeInfo); err != nil { + return err + } + return controller.userAppService.UpdateUserPassword(userId, passwordChangeInfo) + }(); err != nil { + resp := &GeneralResponse{ + Code: constants.RespInternalErr, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, resp) + } else { + resp := &GeneralResponse{ + Code: constants.RespNoErr, + } + c.JSON(http.StatusOK, resp) + } +} diff --git a/site-portal/server/application/service/job_service.go b/site-portal/server/application/service/job_service.go new file mode 100644 index 00000000..c46610ef --- /dev/null +++ b/site-portal/server/application/service/job_service.go @@ -0,0 +1,1035 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/aggregate" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// JobApp provides interface for job related API handling routines +type JobApp struct { + SiteRepo repo.SiteRepository + JobRepo repo.JobRepository + ParticipantRepo repo.JobParticipantRepository + ProjectRepo repo.ProjectRepository + ProjectDataRepo repo.ProjectDataRepository + ModelRepo repo.ModelRepository +} + +// JobInfoBase contains the basic info of a job +type JobInfoBase struct { + Name string `json:"name"` + Description string `json:"description"` + Type entity.JobType `json:"type"` + ProjectUUID string `json:"project_uuid"` +} + +// JobListItemBase contains info of a job for displaying in a list view +type JobListItemBase struct { + JobInfoBase + UUID string `json:"uuid"` + Status entity.JobStatus `json:"status"` + StatusStr string `json:"status_str"` + CreationTime time.Time `json:"creation_time"` + FinishTime time.Time `json:"finish_time"` + InitiatingSiteUUID string `json:"initiating_site_uuid"` + InitiatingSiteName string `json:"initiating_site_name"` + InitiatingSitePartyID uint `json:"initiating_site_party_id"` + PendingOnThisSite bool `json:"pending_on_this_site"` + FATEJobID string `json:"fate_job_id"` + FATEJobStatus string `json:"fate_job_status"` + FATEModelName string `json:"fate_model_name"` + IsInitiator bool `json:"is_initiator"` + Username string `json:"username"` +} + +// JobConf is the configuration content for a FATE job +type JobConf struct { + ConfJson string `json:"conf_json"` + DSLJson string `json:"dsl_json"` +} + +// JobDataBase is the basic info of a job data +type JobDataBase struct { + DataUUID string `json:"data_uuid"` + LabelName string `json:"label_name"` +} + +// JobData contains detailed info of a data used in a job +type JobData struct { + JobDataBase + Name string `json:"name"` + Description string `json:"description"` + ProvidingSiteUUID string `json:"providing_site_uuid"` + ProvidingSiteName string `json:"providing_site_name"` + ProvidingSitePartyID uint `json:"providing_site_party_id"` + IsLocal bool `json:"is_local"` + Status entity.JobParticipantStatus `json:"site_status"` + StatusStr string `json:"site_status_str"` +} + +// JobParticipantInfoBase contains basic information of a job participant +type JobParticipantInfoBase struct { + SiteUUID string `json:"site_uuid"` + SiteName string `json:"site_name"` + SitePartyID uint `json:"site_party_id"` +} + +// TrainingJobResultInfo is a key-value map containing the trained model evaluation info +type TrainingJobResultInfo struct { + EvaluationInfo map[string]string `json:"evaluation_info"` +} + +// PredictingJobResultInfo contains predicting job result data +type PredictingJobResultInfo struct { + Header []string `json:"header"` + Data [][]interface{} `json:"data"` + Count int `json:"count"` +} + +// IntersectionJobResultInfo contains PSI job result data +type IntersectionJobResultInfo struct { + Header []string `json:"header"` + Data [][]interface{} `json:"data"` + IntersectNumber int `json:"intersect_number"` + IntersectRate float64 `json:"intersect_rate"` +} + +// JobAlgorithmConf is the algorithm configuration for a job +type JobAlgorithmConf struct { + ValidationEnabled bool `json:"training_validation_enabled"` + ValidationSizePercent uint `json:"training_validation_percent"` + ModelName string `json:"training_model_name"` + AlgorithmType entity.JobAlgorithmType `json:"training_algorithm_type"` + AlgorithmComponentName string `json:"algorithm_component_name"` + ComponentsToDeploy []string `json:"training_component_list_to_deploy"` + ModelUUID string `json:"predicting_model_uuid"` + EvaluateComponentName string `json:"evaluate_component_name"` +} + +// JobSubmissionRequest is the request for creating a job +type JobSubmissionRequest struct { + JobConf + JobInfoBase + InitiatorData JobDataBase `json:"initiator_data"` + OtherData []JobDataBase `json:"other_site_data"` + JobAlgorithmConf +} + +// RemoteJobCreationRequest is a request for creating a record of a job that is initiated by other sites +type RemoteJobCreationRequest struct { + JobSubmissionRequest + Username string `json:"username"` + UUID string `json:"uuid"` +} + +// JobDetail contains detailed info of a job, including the result and status message +type JobDetail struct { + JobListItemBase + InitiatorData JobData `json:"initiator_data"` + OtherData []JobData `json:"other_site_data"` + StatusMsg string `json:"status_message"` + ResultInfo JobResultInfo `json:"result_info"` + JobConf + JobAlgorithmConf +} + +// JobApprovalContext is the context used for a job approval response +type JobApprovalContext struct { + SiteUUID string `json:"site_uuid"` + Approved bool `json:"approved"` +} + +// JobStatusUpdateContext is the context used for updating a job status +type JobStatusUpdateContext struct { + Status entity.JobStatus `json:"status"` + StatusMessage string `json:"status_message"` + FATEJobID string `json:"fate_job_id"` + FATEJobStatus string `json:"fate_job_status"` + FATEModelID string `json:"fate_model_id"` + FATEModelVersion string `json:"fate_model_version"` + ParticipantStatusMap map[string]entity.JobParticipantStatus `json:"participant_status_map"` +} + +// JobResultInfo contains result information for all types of jobs +type JobResultInfo struct { + IntersectionResult IntersectionJobResultInfo `json:"intersection_result"` + TrainingResult map[string]string `json:"training_result"` + PredictingResult PredictingJobResultInfo `json:"predicting_result"` +} + +// JobRawDagJson describes the DAG the user draw, also contains the configuration of each job component +type JobRawDagJson struct { + RawJson string `json:"raw_json"` +} + +// GenerateJobConfRequest contains 2 parts of the information: 1. the info of the job, like the participants. 2. the +// info of each job component's configuration, such as what penalty function an algorithm use. +type GenerateJobConfRequest struct { + JobConf JobSubmissionRequest `json:"job_conf"` + DagJson JobRawDagJson `json:"dag_json"` +} + +// List returns a list of jobs in the specified project +func (app *JobApp) List(projectUUID string) ([]JobListItemBase, error) { + site, err := app.loadSite() + if err != nil { + return nil, err + } + + jobListInstance, err := app.JobRepo.GetListByProjectUUID(projectUUID) + if err != nil { + return nil, err + } + domainJobList := jobListInstance.([]entity.Job) + + jobList := make([]JobListItemBase, 0) + for _, job := range domainJobList { + if job.Status != entity.JobStatusUnknown && job.Status != entity.JobStatusDeleted { + jobParticipantInstance, err := app.ParticipantRepo.GetByJobAndSiteUUID(job.UUID, site.UUID) + if err != nil { + if err == repo.ErrJobParticipantNotFound { + continue + } + return nil, errors.Wrap(err, "failed to get participant info") + } + jobParticipant := jobParticipantInstance.(*entity.JobParticipant) + + jobList = append(jobList, JobListItemBase{ + JobInfoBase: JobInfoBase{ + Name: job.Name, + Description: job.Description, + Type: job.Type, + ProjectUUID: job.ProjectUUID, + }, + UUID: job.UUID, + Status: job.Status, + StatusStr: job.Status.String(), + CreationTime: job.CreatedAt, + FinishTime: job.FinishedAt, + InitiatingSiteUUID: job.InitiatingSiteUUID, + InitiatingSiteName: job.InitiatingSiteName, + InitiatingSitePartyID: job.InitiatingSitePartyID, + PendingOnThisSite: jobParticipant.Status == entity.JobParticipantStatusPending, + FATEJobID: job.FATEJobID, + FATEJobStatus: job.FATEJobStatus, + FATEModelName: job.FATEModelID + "#" + job.FATEModelVersion, + IsInitiator: job.InitiatingSiteUUID == site.UUID, + Username: job.InitiatingUser, + }) + } + } + return jobList, nil +} + +// GetJobDetail returns the detail info of a job +func (app *JobApp) GetJobDetail(uuid string) (*JobDetail, error) { + site, err := app.loadSite() + if err != nil { + return nil, err + } + + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return nil, err + } + + jobRequest := &JobSubmissionRequest{} + if err := json.Unmarshal([]byte(jobAggregate.Job.RequestJson), jobRequest); err != nil { + return nil, errors.Wrap(err, "failed to recover job submission request") + } + + jobDetail := &JobDetail{ + JobListItemBase: JobListItemBase{ + JobInfoBase: JobInfoBase{ + Name: jobAggregate.Job.Name, + Description: jobAggregate.Job.Description, + Type: jobAggregate.Job.Type, + ProjectUUID: jobAggregate.Job.ProjectUUID, + }, + UUID: jobAggregate.Job.UUID, + Status: jobAggregate.Job.Status, + StatusStr: jobAggregate.Job.Status.String(), + CreationTime: jobAggregate.Job.CreatedAt, + FinishTime: jobAggregate.Job.UpdatedAt, + InitiatingSiteUUID: jobAggregate.Initiator.SiteUUID, + InitiatingSiteName: jobAggregate.Initiator.SiteName, + InitiatingSitePartyID: jobAggregate.Initiator.SitePartyID, + PendingOnThisSite: false, + FATEJobID: jobAggregate.Job.FATEJobID, + FATEJobStatus: jobAggregate.Job.FATEJobStatus, + IsInitiator: jobAggregate.Initiator.SiteUUID == site.UUID, + Username: jobAggregate.Job.InitiatingUser, + FATEModelName: jobAggregate.Job.FATEModelID + "#" + jobAggregate.Job.FATEModelVersion, + }, + InitiatorData: JobData{ + JobDataBase: JobDataBase{ + DataUUID: jobAggregate.Initiator.DataUUID, + LabelName: jobAggregate.Initiator.DataLabelName, + }, + Name: jobAggregate.Initiator.DataName, + Description: jobAggregate.Initiator.DataDescription, + ProvidingSiteUUID: jobAggregate.Initiator.SiteUUID, + ProvidingSiteName: jobAggregate.Initiator.SiteName, + ProvidingSitePartyID: jobAggregate.Initiator.SitePartyID, + IsLocal: jobAggregate.Initiator.SiteUUID == site.UUID, + Status: jobAggregate.Initiator.Status, + StatusStr: jobAggregate.Initiator.Status.String(), + }, + OtherData: nil, + StatusMsg: app.buildJobStatusMessage(jobAggregate), + JobConf: JobConf{ + ConfJson: jobAggregate.Job.Conf, + DSLJson: jobAggregate.Job.DSL, + }, + JobAlgorithmConf: JobAlgorithmConf{ + ValidationEnabled: jobRequest.ValidationEnabled, + ValidationSizePercent: jobRequest.ValidationSizePercent, + ModelName: jobAggregate.Job.ModelName, + AlgorithmType: jobRequest.AlgorithmType, + AlgorithmComponentName: jobRequest.AlgorithmComponentName, + ComponentsToDeploy: jobRequest.ComponentsToDeploy, + ModelUUID: jobRequest.ModelUUID, + }, + } + switch jobAggregate.Job.Type { + case entity.JobTypeTraining: + jobDetail.ResultInfo.TrainingResult = jobAggregate.Job.GetTrainingResultSummary() + case entity.JobTypePredict: + header, data, count := jobAggregate.Job.GetPredictingResultPreview() + jobDetail.ResultInfo.PredictingResult = PredictingJobResultInfo{ + Header: header, + Data: data, + Count: count, + } + case entity.JobTypePSI: + header, data, intersectNumber, intersectRate := jobAggregate.Job.GetIntersectionResult() + jobDetail.ResultInfo.IntersectionResult = IntersectionJobResultInfo{ + Header: header, + Data: data, + IntersectNumber: intersectNumber, + IntersectRate: intersectRate, + } + } + + for _, participant := range jobAggregate.Participants { + jobDetail.OtherData = append(jobDetail.OtherData, JobData{ + JobDataBase: JobDataBase{ + DataUUID: participant.DataUUID, + LabelName: "ignored", + }, + Name: participant.DataName, + Description: participant.DataDescription, + ProvidingSiteUUID: participant.SiteUUID, + ProvidingSiteName: participant.SiteName, + ProvidingSitePartyID: participant.SitePartyID, + IsLocal: participant.SiteUUID == site.UUID, + Status: participant.Status, + StatusStr: participant.Status.String(), + }) + if participant.SiteUUID == site.UUID && participant.Status == entity.JobParticipantStatusPending { + jobDetail.PendingOnThisSite = true + } + } + + return jobDetail, nil +} + +// GetDataResultDownloadRequest returns a request object to be used to download the result data +func (app *JobApp) GetDataResultDownloadRequest(uuid string) (*http.Request, error) { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return nil, err + } + return jobAggregate.GetDataResultDownloadRequest() +} + +// Approve approves the job +func (app *JobApp) Approve(uuid string) error { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return err + } + return jobAggregate.ApproveJob() +} + +// Reject rejects the job +func (app *JobApp) Reject(uuid string) error { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return err + } + return jobAggregate.RejectJob() +} + +// Refresh checks the latest job status +func (app *JobApp) Refresh(uuid string) error { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return err + } + return jobAggregate.RefreshJob() +} + +// GenerateConfig returns the job configuration content based on the job info +func (app *JobApp) GenerateConfig(username string, request *JobSubmissionRequest) (*JobConf, error) { + jobAggregate, err := app.buildJobAggregate(username, request) + if err != nil { + return nil, err + } + + config, dsl, err := jobAggregate.GenerateConfig() + if err != nil { + return nil, errors.Wrap(err, "failed to get job config") + } + return &JobConf{ + ConfJson: config, + DSLJson: dsl, + }, nil +} + +// LoadJobComponents returns s json string, UI can use this json to populate the FML components, so that a user +// can drag the components to define a job. The json contains the default configs for each component. +func (app *JobApp) LoadJobComponents() string { + return constants.JobComponents +} + +// GenerateDslFromDag returns a json string, which is the DSL configuration which can be consumed by fateflow. The +// input is the raw json string which represent the DAG the user has draw on UI. +func (app *JobApp) GenerateDslFromDag(rawJson string) (string, error) { + var dagStruct map[string]interface{} + res := make(map[string]interface{}) + res["components"] = make(map[string]interface{}) + dslComponents := res["components"].(map[string]interface{}) + + err := json.Unmarshal([]byte(rawJson), &dagStruct) + if err != nil { + return "", err + } + + for key, value := range dagStruct { + /* + an example of "value": + { + "attributeType": "common", + "commonAttributes": {}, + "diffAttributes": {}, + "conditions": { + "output": { + "data": ["data"] + } + }, + "module": "Reader" + } + */ + dslComponents[key] = make(map[string]interface{}) + dslComponentContent := dslComponents[key].(map[string]interface{}) + dagValue := value.(map[string]interface{}) + dslComponentContent["module"] = dagValue["module"].(string) + input := dagValue["conditions"].(map[string]interface{})["input"] + if input != nil { + dslComponentContent["input"] = input + } + output := dagValue["conditions"].(map[string]interface{})["output"] + if output != nil { + dslComponentContent["output"] = output + } + /* + an example of "dslComponentContent": + { + "module": "Reader", + "output": { + "data": [ + "data" + ] + } + */ + } + resJson, err := json.Marshal(res) + if err != nil { + return "", err + } + resStr, err := app.generateIndentedJsonStr(string(resJson)) + return resStr, nil +} + +// GenerateConfFromDag returns a map, which represents the configuration of each fate job component. +func (app *JobApp) GenerateConfFromDag( + username string, generateJobConfRequest *GenerateJobConfRequest) (string, error) { + var res map[string]interface{} + jobAggregate, err := app.buildJobAggregate(username, &generateJobConfRequest.JobConf) + numberOfHosts := len(jobAggregate.Participants) + componentParameters, err := app.buildComponentParameters( + generateJobConfRequest.DagJson.RawJson, numberOfHosts) + if err != nil { + return "", errors.Wrap(err, "failed to build the component parameters") + } + + hostUuidList := make([]string, 0) + // The "OtherData" below is a list, the sequence matches the host configurations in above "componentParameters", + // which is generated from the DAG user drawn. For example, if there are 3 hosts in the "otherData", + // then the 1st host's config must match the "0" config in "componentParameters". The 2nd host's config must match + // the "1" config in "componentParameters", etc. So now the problem is, how to make the later-added reader info keep + // the same sequence? This cannot be done by "jobAggregate" because its "Participants" attr is a map. So here we + // need the sequence information from "OtherData". "OtherData" contains the "dataUUID" which can help us find the + // "siteUUID" of each party, in the same sequence with the DAG configurations. Then we can use the siteUUID as the + // key to fetch the table name and namespace from the "Participants" map of "jobAggregate". + for _, party := range generateJobConfRequest.JobConf.OtherData { + projectDataInstance, err := app.ProjectDataRepo.GetByDataUUID(party.DataUUID) + if err != nil { + return "", errors.Wrap(err, "failed to query project data") + } + projectData := projectDataInstance.(*entity.ProjectData) + hostUuidList = append(hostUuidList, projectData.SiteUUID) + } + + generalJobConf, err := jobAggregate.GenerateGeneralTrainingConf(hostUuidList) + if err != nil { + errStr := "failed to generate the general training configurations" + return errStr, errors.Wrap(err, errStr) + } + // Load the general configurations into the res + err = json.Unmarshal([]byte(generalJobConf), &res) + if err != nil { + errStr := "failed to unmarshal the general job conf" + return errStr, errors.Wrap(err, errStr) + } + + hostReaderConfigMap, guestReaderConfigMap := jobAggregate.GenerateReaderConfigMaps(hostUuidList) + // Replace the readers' config in "componentParameters" with the ones in defaultHostConfMap and defaultGuestConfMap + hostConfigs := componentParameters["component_parameters"].(map[string]interface{})["role"].(map[string]interface{})["host"].(map[string]interface{}) + guestConfigs := componentParameters["component_parameters"].(map[string]interface{})["role"].(map[string]interface{})["guest"].(map[string]interface{}) + // for above 2 maps, the key should be an index like "0", "1", etc. + for index, hostConfig := range hostConfigs { + hostConfig.(map[string]interface{})["reader_0"] = hostReaderConfigMap[index].(map[string]interface{})["reader_0"] + } + for index, guestConfig := range guestConfigs { + guestConfig.(map[string]interface{})["reader_0"] = guestReaderConfigMap[index].(map[string]interface{})["reader_0"] + } + for key, value := range componentParameters { + res[key] = value + } + resJson, err := json.Marshal(res) + if err != nil { + return "", err + } + resStr, err := app.generateIndentedJsonStr(string(resJson)) + return resStr, nil +} + +// buildComponentParameters is a helper function that helps to handle the common parameters and diff parameters the +// use has chosen on the DAG, and generate the format Fateflow can understand. +func (app *JobApp) buildComponentParameters(dagJsonStr string, numberOfHosts int) (map[string]interface{}, error) { + res := make(map[string]interface{}) + res["component_parameters"] = make(map[string]interface{}) + componentParameters := res["component_parameters"].(map[string]interface{}) + componentParameters["common"] = make(map[string]interface{}) + componentParameters["role"] = make(map[string]interface{}) + common := componentParameters["common"].(map[string]interface{}) + role := componentParameters["role"].(map[string]interface{}) + role["host"] = make(map[string]interface{}) + role["guest"] = make(map[string]interface{}) + host := role["host"].(map[string]interface{}) + guest := role["guest"].(map[string]interface{}) + // There must be only one guest + guest["0"] = make(map[string]interface{}) + for i := 0; i < numberOfHosts; i++ { + // There can be more than one host + host[strconv.Itoa(i)] = make(map[string]interface{}) + } + var dagMap map[string]interface{} + err := json.Unmarshal([]byte(dagJsonStr), &dagMap) + if err != nil { + return nil, err + } + for dagKey, dagValue := range dagMap { + // dagKey is something like: "HomoLR_0" + // dagValue is a map of 5 fixed keys: attributeType, commonAttributes, diffAttributes, conditions, module + dagConfs := dagValue.(map[string]interface{}) + if dagConfs["attributeType"] == "common" { + common[dagKey] = make(map[string]interface{}) + dagCommonAttrs := dagConfs["commonAttributes"].(map[string]interface{}) + commonAttrMap := app.filterEmptyAttributes(dagCommonAttrs) + common[dagKey] = commonAttrMap + } else if dagConfs["attributeType"] == "diff" { + dagDiffAttrs := dagConfs["diffAttributes"].(map[string]interface{}) + // for dagDiffAttrs, the keys will be like: guest, host_0, host_1, etc. + if dagConfs["module"].(string) == "Reader" { + // The reason for this special case is: The reader's configuration will be copied from the default + // configuration later. UI will not pass the table name/namespace here, so no need to handle here. + // Also, for reader there is no attributes to choose from UI, so we can skip below code. + continue + } + // guest is the simple one, handle it first + dagGuestDiffAttrs := dagDiffAttrs["guest"].(map[string]interface{}) + guestConfMap := role["guest"].(map[string]interface{})["0"].(map[string]interface{}) + guestConfMap[dagKey] = app.filterEmptyAttributes(dagGuestDiffAttrs) + // remove the guest item from the dagDiffAttrs, make it easier to handle the hosts + delete(dagDiffAttrs, "guest") + for dagHostName, dagHostDiffAttrMap := range dagDiffAttrs { + // dagHostName must be like host_0, host_1, host_2, etc. + hostIndexStr := strings.Split(dagHostName, "_")[1] + roleHostMap := role["host"].(map[string]interface{}) + // roleHostMap's key is "0", "1", "2", etc. + hostConfMap := roleHostMap[hostIndexStr].(map[string]interface{}) + hostConfMap[dagKey] = app.filterEmptyAttributes(dagHostDiffAttrMap.(map[string]interface{})) + } + } else { + return nil, errors.New("Undefined attribute type in the dag") + } + } + return res, nil +} + +// filterEmptyAttributes is a helper function that helps to filter out the empty attributes of a Fateflow component +func (app *JobApp) filterEmptyAttributes(source map[string]interface{}) map[string]interface{} { + target := make(map[string]interface{}) + for k, v := range source { + if !app.isEmpty(v) { + target[k] = v + } + } + return target +} + +// isEmpty is a helper function to help check if an attribute of a Fateflow component is {}, [] or "" +func (app *JobApp) isEmpty(object interface{}) bool { + if object == nil { + return true + } + objectType := reflect.TypeOf(object) + kind := objectType.Kind() + switch kind { + case reflect.Slice: + return len(object.([]interface{})) == 0 + case reflect.Map: + return len(object.(map[string]interface{})) == 0 + case reflect.String: + return object == "" + } + return false +} + +func (app *JobApp) generateIndentedJsonStr(originalJsonStr string) (string, error) { + var prettyJson bytes.Buffer + if err := json.Indent(&prettyJson, []byte(originalJsonStr), "", " "); err != nil { + return "", err + } + return prettyJson.String(), nil +} + +// GeneratePredictingJobParticipants returns a list of participants that should be used in a predicting job +func (app *JobApp) GeneratePredictingJobParticipants(modelUUID string) ([]JobParticipantInfoBase, error) { + if modelUUID == "" { + return nil, errors.New("invalid model uuid") + } + modelEntityInstance, err := app.ModelRepo.GetByUUID(modelUUID) + if err != nil { + return nil, err + } + modelEntity := modelEntityInstance.(*entity.Model) + jobUUID := modelEntity.JobUUID + jobAggregate, err := app.loadJobAggregate(jobUUID) + if err != nil { + return nil, err + } + participantList, err := jobAggregate.GeneratePredictingJobParticipants() + if err != nil { + return nil, err + } + var participantInfoList []JobParticipantInfoBase + for _, participant := range participantList { + // TODO: query from project participant repo to get the latest site info + participantInfoList = append(participantInfoList, JobParticipantInfoBase{ + SiteUUID: participant.SiteUUID, + SiteName: participant.SiteName, + SitePartyID: participant.SitePartyID, + }) + } + return participantInfoList, nil +} + +// SubmitJob creates the job +func (app *JobApp) SubmitJob(username string, request *JobSubmissionRequest) (*JobListItemBase, error) { + jobAggregate, err := app.buildJobAggregate(username, request) + if err != nil { + return nil, err + } + if err := jobAggregate.SubmitJob(); err != nil { + return nil, err + } + + return &JobListItemBase{ + JobInfoBase: JobInfoBase{}, + UUID: jobAggregate.Job.UUID, + Status: jobAggregate.Job.Status, + CreationTime: jobAggregate.Job.CreatedAt, + FinishTime: time.Time{}, + InitiatingSiteUUID: jobAggregate.Job.InitiatingSiteUUID, + InitiatingSiteName: jobAggregate.Job.InitiatingSiteName, + InitiatingSitePartyID: jobAggregate.Job.InitiatingSitePartyID, + PendingOnThisSite: false, + FATEJobID: jobAggregate.Job.FATEJobID, + FATEJobStatus: jobAggregate.Job.FATEJobStatus, + IsInitiator: true, + Username: username, + }, nil +} + +// ProcessNewRemoteJob processes the remote job creation request +func (app *JobApp) ProcessNewRemoteJob(request *RemoteJobCreationRequest) error { + jobAggregate, err := app.buildJobAggregate(request.Username, &request.JobSubmissionRequest) + if err != nil { + return err + } + jobAggregate.Job.UUID = request.UUID + return jobAggregate.HandleRemoteJobCreation() +} + +// ProcessJobResponse handles job approval response +func (app *JobApp) ProcessJobResponse(uuid string, context *JobApprovalContext) error { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return err + } + return jobAggregate.HandleJobApprovalResponse(context.SiteUUID, context.Approved) +} + +// ProcessJobStatusUpdate handles job status update requests +func (app *JobApp) ProcessJobStatusUpdate(uuid string, context *JobStatusUpdateContext) error { + jobAggregate, err := app.loadJobAggregate(uuid) + if err != nil { + return err + } + newJobStatusTemplate := &entity.Job{ + Status: context.Status, + StatusMessage: context.StatusMessage, + FATEJobID: context.FATEJobID, + FATEJobStatus: context.FATEJobStatus, + FATEModelID: context.FATEModelID, + FATEModelVersion: context.FATEModelVersion, + } + return jobAggregate.HandleJobStatusUpdate(newJobStatusTemplate, context.ParticipantStatusMap) +} + +// loadSite is a helper function to return site entity object +func (app *JobApp) loadSite() (*entity.Site, error) { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return nil, errors.Wrapf(err, "failed to load site info") + } + return site, nil +} + +// buildJobAggregate is a helper function to build the job aggregate from the job submission request +func (app *JobApp) buildJobAggregate(username string, request *JobSubmissionRequest) (*aggregate.JobAggregate, error) { + site, err := app.loadSite() + if err != nil { + return nil, err + } + + projectDataInstance, err := app.ProjectDataRepo.GetByDataUUID(request.InitiatorData.DataUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get initiator data") + } + projectData := projectDataInstance.(*entity.ProjectData) + if request.ProjectUUID == "" { + log.Warn().Msgf("missing project uuid, using the latest record") + request.ProjectUUID = projectData.ProjectUUID + } + project, err := app.loadProject(request.ProjectUUID) + if err != nil { + return nil, err + } + + requestJsonByte, err := json.MarshalIndent(request, "", " ") + if err != nil { + return nil, err + } + requestJsonStr := string(requestJsonByte) + + jobAggregate := &aggregate.JobAggregate{ + Job: &entity.Job{ + Name: request.Name, + Description: request.Description, + ProjectUUID: request.ProjectUUID, + Type: request.Type, + AlgorithmType: request.AlgorithmType, + AlgorithmComponentName: request.AlgorithmComponentName, + EvaluateComponentName: request.EvaluateComponentName, + AlgorithmConfig: valueobject.AlgorithmConfig{ + TrainingValidationEnabled: request.ValidationEnabled, + TrainingValidationSizePercent: request.ValidationSizePercent, + TrainingComponentsToDeploy: request.ComponentsToDeploy, + }, + ModelName: request.ModelName, + PredictingModelUUID: request.ModelUUID, + InitiatingSiteUUID: projectData.SiteUUID, + InitiatingSiteName: projectData.SiteName, + InitiatingSitePartyID: projectData.SitePartyID, + InitiatingUser: username, + IsInitiatingSite: site.UUID == projectData.SiteUUID, + Conf: request.ConfJson, + DSL: request.DSLJson, + RequestJson: requestJsonStr, + FATEFlowContext: entity.FATEFlowContext{ + FATEFlowHost: site.FATEFlowHost, + FATEFlowPort: site.FATEFlowHTTPPort, + FATEFlowIsHttps: false, + }, + Repo: app.JobRepo, + }, + Initiator: &entity.JobParticipant{ + SiteUUID: projectData.SiteUUID, + SiteName: projectData.SiteName, + SitePartyID: projectData.SitePartyID, + SiteRole: entity.JobParticipantRoleGuest, + DataUUID: projectData.DataUUID, + DataName: projectData.Name, + DataDescription: projectData.Description, + DataTableName: projectData.TableName, + DataTableNamespace: projectData.TableNamespace, + DataLabelName: request.InitiatorData.LabelName, + Status: entity.JobParticipantStatusInitiator, + Repo: app.ParticipantRepo, + }, + Participants: map[string]*entity.JobParticipant{}, + JobRepo: app.JobRepo, + ParticipantRepo: app.ParticipantRepo, + FMLManagerConnectionInfo: aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + JobContext: aggregate.JobContext{ + AutoApprovalEnabled: project.AutoApprovalEnabled, + CurrentSiteUUID: site.UUID, + }, + } + + for _, otherData := range request.OtherData { + projectDataInstance, err = app.ProjectDataRepo.GetByDataUUID(otherData.DataUUID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get other site data: %s", otherData.DataUUID) + } + projectData = projectDataInstance.(*entity.ProjectData) + jobAggregate.Participants[projectData.SiteUUID] = &entity.JobParticipant{ + SiteUUID: projectData.SiteUUID, + SiteName: projectData.SiteName, + SitePartyID: projectData.SitePartyID, + SiteRole: entity.JobParticipantRoleHost, + DataUUID: projectData.DataUUID, + DataName: projectData.Name, + DataDescription: projectData.Description, + DataTableName: projectData.TableName, + DataTableNamespace: projectData.TableNamespace, + DataLabelName: request.InitiatorData.LabelName, + Status: entity.JobParticipantStatusPending, + Repo: app.ParticipantRepo, + } + } + + if request.Type == entity.JobTypePredict { + log.Info().Msgf("changing participants role and job info according to original job info") + if request.ModelUUID == "" { + return nil, errors.New("invalid model uuid") + } + modelEntityInstance, err := app.ModelRepo.GetByUUID(request.ModelUUID) + if err != nil { + return nil, err + } + modelEntity := modelEntityInstance.(*entity.Model) + jobUUID := modelEntity.JobUUID + + participant, err := app.loadJobParticipant(jobUUID, jobAggregate.Initiator.SiteUUID) + if err != nil { + return nil, err + } + jobAggregate.Initiator.SiteRole = participant.SiteRole + + for siteUUID := range jobAggregate.Participants { + participant, err = app.loadJobParticipant(jobUUID, siteUUID) + if err != nil { + return nil, err + } + jobAggregate.Participants[siteUUID].SiteRole = participant.SiteRole + } + + jobInstance, err := app.JobRepo.GetByUUID(jobUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + + jobAggregate.Job.FATEModelID = modelEntity.FATEModelID + jobAggregate.Job.FATEModelVersion = modelEntity.FATEModelVersion + jobAggregate.Job.ModelName = job.ModelName + jobAggregate.Job.AlgorithmType = job.AlgorithmType + jobAggregate.Job.AlgorithmComponentName = job.AlgorithmComponentName + } + + return jobAggregate, nil +} + +// loadJobAggregate is a helper function to build the job aggregate from the info in the repo +func (app *JobApp) loadJobAggregate(jobUUID string) (*aggregate.JobAggregate, error) { + site, err := app.loadSite() + if err != nil { + return nil, err + } + + jobInstance, err := app.JobRepo.GetByUUID(jobUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + job.Repo = app.JobRepo + job.FATEFlowContext = entity.FATEFlowContext{ + FATEFlowHost: site.FATEFlowHost, + FATEFlowPort: site.FATEFlowHTTPPort, + FATEFlowIsHttps: false, + } + + project, err := app.loadProject(job.ProjectUUID) + if err != nil { + return nil, err + } + + jobAggregate := &aggregate.JobAggregate{ + Job: job, + Initiator: nil, + Participants: map[string]*entity.JobParticipant{}, + JobRepo: app.JobRepo, + ParticipantRepo: app.ParticipantRepo, + FMLManagerConnectionInfo: aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + JobContext: aggregate.JobContext{ + AutoApprovalEnabled: project.AutoApprovalEnabled, + CurrentSiteUUID: site.UUID, + }, + } + + participantListInstance, err := app.ParticipantRepo.GetListByJobUUID(jobUUID) + if err != nil { + return nil, err + } + participantList := participantListInstance.([]entity.JobParticipant) + for index, participant := range participantList { + participantList[index].Repo = app.ParticipantRepo + if participant.SiteUUID == jobAggregate.Job.InitiatingSiteUUID { + jobAggregate.Initiator = &participantList[index] + } else { + jobAggregate.Participants[participant.SiteUUID] = &participantList[index] + } + } + return jobAggregate, nil +} + +// buildJobStatusMessage builds the status message based on the job and participant status +func (app *JobApp) buildJobStatusMessage(jobAggregate *aggregate.JobAggregate) string { + switch jobAggregate.Job.Status { + case entity.JobStatusUnknown: + return "Job status unknown" + case entity.JobStatusSucceeded: + return "Job succeeded" + case entity.JobStatusFailed: + return "Job failed: " + jobAggregate.Job.StatusMessage + case entity.JobStatusRunning: + return "Job is running" + } + if jobAggregate.Job.Status == entity.JobStatusRejected { + rejectedParticipantStr := "" + for _, participant := range jobAggregate.Participants { + if participant.Status == entity.JobParticipantStatusRejected { + participantStr := fmt.Sprintf("%s(%d)", participant.SiteName, participant.SitePartyID) + if rejectedParticipantStr == "" { + rejectedParticipantStr += participantStr + } else { + rejectedParticipantStr += ", " + participantStr + } + } + } + return "Job is rejected by " + rejectedParticipantStr + } + if jobAggregate.Job.Status == entity.JobStatusPending { + pendingParticipantStr := "" + for _, participant := range jobAggregate.Participants { + if participant.Status == entity.JobParticipantStatusPending { + participantStr := fmt.Sprintf("%s(%d)", participant.SiteName, participant.SitePartyID) + if pendingParticipantStr == "" { + pendingParticipantStr += participantStr + } else { + pendingParticipantStr += ", " + participantStr + } + } + } + if pendingParticipantStr == "" { + return "Job is waiting to start" + } else { + return "Job is pending on " + pendingParticipantStr + } + } + return fmt.Sprintf(`Job status "%s" unknown`, jobAggregate.Job.Status.String()) +} + +// loadProject loads and returns the project entity from the repo +func (app *JobApp) loadProject(projectUUID string) (*entity.Project, error) { + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query project") + } + return projectInstance.(*entity.Project), nil +} + +// loadJobParticipant loads and return the job participant entity +func (app *JobApp) loadJobParticipant(jobUUID, siteUUID string) (*entity.JobParticipant, error) { + participantInstance, err := app.ParticipantRepo.GetByJobAndSiteUUID(jobUUID, siteUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query participant") + } + participant := participantInstance.(*entity.JobParticipant) + return participant, nil +} + +// DeleteJob mark the job status as deleted +func (app *JobApp) DeleteJob(jobUUID string) error { + jobInstance, err := app.JobRepo.GetByUUID(jobUUID) + if err != nil { + return errors.Wrap(err, "failed to query job") + } + job := jobInstance.(*entity.Job) + job.Repo = app.JobRepo + return job.UpdateStatus(entity.JobStatusDeleted) +} diff --git a/site-portal/server/application/service/job_service_test.go b/site-portal/server/application/service/job_service_test.go new file mode 100644 index 00000000..54ba8c24 --- /dev/null +++ b/site-portal/server/application/service/job_service_test.go @@ -0,0 +1,890 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func getTwoSiteInput() string { + input := `{ + "reader_0": { + "attributeType": "diff", + "commonAttributes": {}, + "diffAttributes": { + "guest": {}, + "host_0": {} + }, + "conditions": { + "output": { + "data": ["data"] + } + }, + "module": "Reader" + }, + "DataIO_0": { + "attributeType": "diff", + "commonAttributes": {}, + "diffAttributes": { + "guest": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": false, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }, + "host_0": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": true, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + } + }, + "conditions": { + "input": { + "data": { + "data": ["reader_0.data"] + } + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + "module": "DataIO" + }, + "HomoLR_0": { + "attributeType": "diff", + "commonAttributes": {}, + "diffAttributes": { + "guest": { + "penalty": "L2", + "tol": 1e-4, + "alpha": 1.0, + "optimizer": "rmsprop", + "batch_size": -1, + "learning_rate": 0.01, + "max_iter": 100, + "early_stop": "diff", + "decay": 1, + "decay_sqrt": true, + "encrypt_param": "{\"method\": null}", + "predict_param": "{\"method\": null}", + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "multi_class": "ovr", + "validation_freqs": "", + "early_stopping_rounds": "", + "metrics": "", + "use_first_metric_only": false, + "re_encrypt_batches": 2, + "aggregate_iters": 1, + "use_proximal": false, + "mu": 0.1 + }, + "host_0": { + "penalty": "L2", + "tol": 1e-4, + "alpha": 1.0, + "optimizer": "rmsprop", + "batch_size": -1, + "learning_rate": 0.1, + "max_iter": 100, + "early_stop": "diff", + "decay": 1, + "decay_sqrt": true, + "encrypt_param": "{\"method\": null}", + "predict_param": "{\"method\": null}", + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "multi_class": "ovr", + "validation_freqs": "", + "early_stopping_rounds": "", + "metrics": "", + "use_first_metric_only": false, + "re_encrypt_batches": 2, + "aggregate_iters": 1, + "use_proximal": false, + "mu": 0.1 + } + }, + "conditions": { + "input": { + "data": { + "data": ["DataIO_0.data"] + } + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + "module": "HomoLR" + }, + "Evaluation_0": { + "attributeType": "common", + "commonAttributes": { + "eval_type": "binary", + "unfold_multi_result": false, + "pos_label": "1", + "need_run": true + }, + "diffAttributes": {}, + "conditions": { + "input": { + "data": { + "data": ["HomoLR_0.data"] + } + }, + "output": { + "data": ["data"] + } + }, + "module": "Evaluation" + } + }` + return input +} + +func getSingleSiteInput() string { + input := `{ + "reader_0": { + "attributeType": "common", + "commonAttributes": {}, + "diffAttributes": {}, + "conditions": { + "output": { + "data": [ + "data" + ] + } + }, + "module": "Reader" + }, + "DataIO_0": { + "attributeType": "common", + "commonAttributes": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": false, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": true, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }, + "diffAttributes": {}, + "conditions": { + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "module": "DataIO" + }, + "HomoSecureboost_0": { + "attributeType": "common", + "commonAttributes": { + "task_type": "classification", + "objective_param": { + "objective": "cross_entropy" + }, + "learning_rate": 0.3, + "num_trees": 5, + "subsample_feature_rate": 1, + "n_iter_no_change": true, + "bin_num": 32, + "validation_freqs": 1, + "tree_param": { + "max_depth": 3 + } + }, + "diffAttributes": {}, + "conditions": { + "input": { + "data": { + "train_data": [ + "DataIO_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "module": "HomoSecureboost" + }, + "Evaluation_0": { + "attributeType": "common", + "commonAttributes": { + "eval_type": "binary", + "unfold_multi_result": false, + "pos_label": 1, + "need_run": true + }, + "diffAttributes": {}, + "conditions": { + "input": { + "data": { + "data": [ + "HomoSecureboost_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ], + "model": [] + } + }, + "module": "Evaluation" + } +}` + return input +} + +func getThreeSiteInput() string { + input := `{ + "reader_0": { + "attributeType": "diff", + "commonAttributes": {}, + "diffAttributes": { + "guest": {}, + "host_0": {}, + "host_1": {} + }, + "conditions": { + "output": { + "data": ["data"] + } + }, + "module": "Reader" + }, + "DataIO_0": { + "attributeType": "diff", + "commonAttributes": {}, + "diffAttributes": { + "guest": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": false, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }, + "host_0": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": true, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }, + "host_1": { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": true, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + } + }, + "conditions": { + "input": { + "data": { + "data": ["reader_0.data"] + } + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + "module": "DataIO" + }, + "HomoLR_0": { + "attributeType": "common", + "diffAttributes": {}, + "commonAttributes": { + "guest": { + "penalty": "L2", + "tol": 1e-4, + "alpha": 1.0, + "optimizer": "rmsprop", + "batch_size": -1, + "learning_rate": 0.01, + "max_iter": 100, + "early_stop": "diff", + "decay": 1, + "decay_sqrt": true, + "encrypt_param": "{\"method\": null}", + "predict_param": "{\"method\": null}", + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "multi_class": "ovr", + "validation_freqs": "", + "early_stopping_rounds": "", + "metrics": "", + "use_first_metric_only": false, + "re_encrypt_batches": 2, + "aggregate_iters": 1, + "use_proximal": false, + "mu": 0.1 + } + }, + "conditions": { + "input": { + "data": { + "data": ["DataIO_0.data"] + } + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + "module": "HomoLR" + }, + "Evaluation_0": { + "attributeType": "common", + "commonAttributes": { + "eval_type": "binary", + "unfold_multi_result": false, + "pos_label": "1", + "need_run": true + }, + "diffAttributes": {}, + "conditions": { + "input": { + "data": { + "data": ["HomoLR_0.data"] + } + }, + "output": { + "data": ["data"] + } + }, + "module": "Evaluation" + } + }` + return input +} + +func TestGenerateDslFromDag(t *testing.T) { + input := getTwoSiteInput() + expected := ` + { + "components": { + "DataIO_0": { + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "module": "DataIO", + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "Evaluation_0": { + "input": { + "data": { + "data": [ + "HomoLR_0.data" + ] + } + }, + "module": "Evaluation", + "output": { + "data": [ + "data" + ] + } + }, + "HomoLR_0": { + "input": { + "data": { + "data": [ + "DataIO_0.data" + ] + } + }, + "module": "HomoLR", + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "reader_0": { + "module": "Reader", + "output": { + "data": [ + "data" + ] + } + } + } + }` + var expectedStruct, actualStruct map[string]interface{} + jobApp := JobApp{} + actual, _ := jobApp.GenerateDslFromDag(input) + json.Unmarshal([]byte(expected), &expectedStruct) + json.Unmarshal([]byte(actual), &actualStruct) + assert.True(t, reflect.DeepEqual(actualStruct, expectedStruct)) +} + +func TestBuildComponentParametersTwoSites(t *testing.T) { + jobApp := JobApp{} + input := getTwoSiteInput() + expected := `{ + "component_parameters": { + "common": { + "Evaluation_0": { + "eval_type": "binary", + "need_run": true, + "pos_label": "1", + "unfold_multi_result": false + } + }, + "role": { + "guest": { + "0": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": false, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": false + }, + "HomoLR_0": { + "aggregate_iters": 1, + "alpha": 1, + "batch_size": -1, + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "decay": 1, + "decay_sqrt": true, + "early_stop": "diff", + "encrypt_param": "{\"method\": null}", + "learning_rate": 0.01, + "max_iter": 100, + "mu": 0.1, + "multi_class": "ovr", + "optimizer": "rmsprop", + "penalty": "L2", + "predict_param": "{\"method\": null}", + "re_encrypt_batches": 2, + "tol": 0.0001, + "use_first_metric_only": false, + "use_proximal": false + } + } + }, + "host": { + "0": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": true, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": false + }, + "HomoLR_0": { + "aggregate_iters": 1, + "alpha": 1, + "batch_size": -1, + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "decay": 1, + "decay_sqrt": true, + "early_stop": "diff", + "encrypt_param": "{\"method\": null}", + "learning_rate": 0.1, + "max_iter": 100, + "mu": 0.1, + "multi_class": "ovr", + "optimizer": "rmsprop", + "penalty": "L2", + "predict_param": "{\"method\": null}", + "re_encrypt_batches": 2, + "tol": 0.0001, + "use_first_metric_only": false, + "use_proximal": false + } + } + } + } + } +}` + var expectedStruct, actualStruct map[string]interface{} + actualStruct, _ = jobApp.buildComponentParameters(input, 1) + json.Unmarshal([]byte(expected), &expectedStruct) + assert.True(t, reflect.DeepEqual(actualStruct, expectedStruct)) +} + +func TestBuildComponentParametersSingleSite(t *testing.T) { + jobApp := JobApp{} + input := getSingleSiteInput() + var actualStruct, expectedStruct map[string]interface{} + expected := `{ + "component_parameters": { + "common": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": false, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": true + }, + "Evaluation_0": { + "eval_type": "binary", + "need_run": true, + "pos_label": 1, + "unfold_multi_result": false + }, + "HomoSecureboost_0": { + "bin_num": 32, + "learning_rate": 0.3, + "n_iter_no_change": true, + "num_trees": 5, + "objective_param": { + "objective": "cross_entropy" + }, + "subsample_feature_rate": 1, + "task_type": "classification", + "tree_param": { + "max_depth": 3 + }, + "validation_freqs": 1 + }, + "reader_0": {} + }, + "role": { + "guest": { + "0": {} + }, + "host": {} + } + } +}` + actualStruct, _ = jobApp.buildComponentParameters(input, 0) + json.Unmarshal([]byte(expected), &expectedStruct) + assert.True(t, reflect.DeepEqual(actualStruct, expectedStruct)) +} + +func TestBuildComponentParametersThreeSites(t *testing.T) { + jobApp := JobApp{} + input := getThreeSiteInput() + var actualStruct, expectedStruct map[string]interface{} + expected := `{ + "component_parameters": { + "common": { + "Evaluation_0": { + "eval_type": "binary", + "need_run": true, + "pos_label": "1", + "unfold_multi_result": false + }, + "HomoLR_0": { + "guest": { + "aggregate_iters": 1, + "alpha": 1, + "batch_size": -1, + "callback_param": "{\"method\": null}", + "cv_param": "{\"n_splits\": 4, \"shuffle\": true, \"random_seed\": 33, \"need_cv\": false}", + "decay": 1, + "decay_sqrt": true, + "early_stop": "diff", + "early_stopping_rounds": "", + "encrypt_param": "{\"method\": null}", + "learning_rate": 0.01, + "max_iter": 100, + "metrics": "", + "mu": 0.1, + "multi_class": "ovr", + "optimizer": "rmsprop", + "penalty": "L2", + "predict_param": "{\"method\": null}", + "re_encrypt_batches": 2, + "tol": 0.0001, + "use_first_metric_only": false, + "use_proximal": false, + "validation_freqs": "" + } + } + }, + "role": { + "guest": { + "0": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": false, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": false + } + } + }, + "host": { + "0": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": true, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": false + } + }, + "1": { + "DataIO_0": { + "data_type": "float64", + "default_value": 0, + "delimitor": ",", + "input_format": "dense", + "label_name": "y", + "label_type": "int", + "missing_fill": false, + "outlier_replace": true, + "output_format": "dense", + "tag_value_delimitor": ":", + "tag_with_value": false, + "with_label": false + } + } + } + } + } +}` + actualStruct, _ = jobApp.buildComponentParameters(input, 2) + json.Unmarshal([]byte(expected), &expectedStruct) + assert.True(t, reflect.DeepEqual(actualStruct, expectedStruct)) +} + +func TestIsEmpty(t *testing.T) { + jobApp := JobApp{} + // Test String + output := jobApp.isEmpty("") + assert.True(t, output) + output = jobApp.isEmpty("str") + assert.False(t, output) + + // Test Slice + var testSlice []interface{} + output = jobApp.isEmpty(testSlice) + assert.True(t, output) + testSlice = append(testSlice, "hello") + output = jobApp.isEmpty(testSlice) + assert.False(t, output) + + // Test Map + testMap := make(map[string]interface{}) + output = jobApp.isEmpty(testMap) + assert.True(t, output) + testMap["key"] = make([]interface{}, 0) + output = jobApp.isEmpty(testMap) +} + +func TestFilterEmptyAttributes(t *testing.T) { + jobApp := JobApp{} + inputStr := ` + { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": false, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }` + expectedStr := ` + { + "input_format": "dense", + "delimitor": ",", + "data_type": "float64", + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "outlier_replace": false, + "with_label": false, + "label_name": "y", + "label_type": "int", + "output_format": "dense" + }` + var inputStruct, expectedStruct map[string]interface{} + json.Unmarshal([]byte(inputStr), &inputStruct) + json.Unmarshal([]byte(expectedStr), &expectedStruct) + actualOutputStruct := jobApp.filterEmptyAttributes(inputStruct) + assert.True(t, reflect.DeepEqual(expectedStruct, actualOutputStruct)) +} + +func TestGenerateIndentedJsonStr(t *testing.T) { + jobApp := JobApp{} + originalJsonStr := "{\"Evaluation_0\":{\"eval_type\":\"binary\",\"need_run\":true,\"pos_label\":\"1\",\"unfold_multi_result\":false}}" + expectetStr := "{\n \"Evaluation_0\": {\n \"eval_type\": \"binary\",\n \"need_run\": true,\n \"pos_label\": \"1\",\n \"unfold_multi_result\": false\n }\n}" + actualStr, _ := jobApp.generateIndentedJsonStr(originalJsonStr) + assert.Equal(t, expectetStr, actualStr) +} diff --git a/site-portal/server/application/service/local_data_service.go b/site-portal/server/application/service/local_data_service.go new file mode 100644 index 00000000..7e6c1407 --- /dev/null +++ b/site-portal/server/application/service/local_data_service.go @@ -0,0 +1,224 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "fmt" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" +) + +// LocalDataApp provides local data management services +type LocalDataApp struct { + LocalDataRepo repo.LocalDataRepository + SiteRepo repo.SiteRepository + ProjectRepo repo.ProjectRepository + ProjectDataRepo repo.ProjectDataRepository +} + +// LocalDataUploadRequest contains basic upload request information +type LocalDataUploadRequest struct { + Name string `form:"name"` + Description string `form:"description"` + FileHeader *multipart.FileHeader +} + +// LocalDataListItem is an item describing a data record +type LocalDataListItem struct { + Name string `json:"name"` + DataID string `json:"data_id"` + CreationTime time.Time `json:"creation_time"` + SampleSize uint64 `json:"sample_size"` + FeatureSize int `json:"feature_size"` + UploadJobStatus entity.UploadJobStatus `json:"upload_job_status"` +} + +// LocalDataDetail contains local data details +type LocalDataDetail struct { + LocalDataListItem + Description string `json:"description"` + TableName string `json:"table_name"` + Filename string `json:"filename"` + IDMetaInfo *valueobject.IDMetaInfo `json:"id_meta_info"` + Features []string `json:"features_array"` + Preview string `json:"preview_array"` +} + +// LocalDataIDMetaInfoUpdateRequest contains basic upload request information +type LocalDataIDMetaInfoUpdateRequest struct { + *valueobject.IDMetaInfo +} + +// Upload loads FATE flow connection info and calls into local data domain object to +// upload the data into the FATE system +func (s *LocalDataApp) Upload(request *LocalDataUploadRequest) (string, error) { + site := entity.Site{ + Repo: s.SiteRepo, + } + if err := site.Load(); err != nil { + return "", errors.Wrapf(err, "failed to load connection info of FATE flow") + } + context := entity.UploadContext{ + FATEFlowHost: site.FATEFlowHost, + FATEFlowPort: site.FATEFlowHTTPPort, + FATEFlowIsHttps: false, + } + data := entity.LocalData{ + Name: request.Name, + Description: request.Description, + UploadContext: context, + Repo: s.LocalDataRepo, + } + if err := data.Upload(request.FileHeader); err != nil { + return "", err + } + return data.UUID, nil +} + +// List return the list of data uploaded historically +func (s *LocalDataApp) List() ([]LocalDataListItem, error) { + instanceList, err := s.LocalDataRepo.GetAll() + if err != nil { + return nil, err + } + dataList := instanceList.([]entity.LocalData) + publicDataList := make([]LocalDataListItem, len(dataList)) + for index, item := range dataList { + publicDataList[index] = LocalDataListItem{ + Name: item.Name, + DataID: item.UUID, + CreationTime: item.CreatedAt, + SampleSize: item.Count, + FeatureSize: len(item.Features), + UploadJobStatus: item.JobStatus, + } + } + return publicDataList, nil +} + +// Get returns the detailed information of a data record +func (s *LocalDataApp) Get(uuid string) (*LocalDataDetail, error) { + instance, err := s.LocalDataRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + localData := instance.(*entity.LocalData) + localDataDetail := &LocalDataDetail{ + LocalDataListItem: LocalDataListItem{ + Name: localData.Name, + DataID: localData.UUID, + CreationTime: localData.CreatedAt, + SampleSize: localData.Count, + FeatureSize: len(localData.Features), + UploadJobStatus: localData.JobStatus, + }, + Description: localData.Description, + TableName: fmt.Sprintf("%s#%s", localData.TableNamespace, localData.TableName), + Filename: filepath.Base(localData.LocalFilePath), + IDMetaInfo: localData.IDMetaInfo, + Features: localData.Features, + Preview: localData.Preview, + } + + return localDataDetail, nil +} + +// GetFilePath returns absolute file path of the stored local data file +func (s *LocalDataApp) GetFilePath(uuid string) (string, error) { + instance, err := s.LocalDataRepo.GetByUUID(uuid) + if err != nil { + return "", err + } + localData := instance.(*entity.LocalData) + return localData.GetAbsFilePath() +} + +// GetColumns returns a list of headers of the current data +func (s *LocalDataApp) GetColumns(uuid string) ([]string, error) { + instance, err := s.LocalDataRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + localData := instance.(*entity.LocalData) + var columns []string + for _, header := range localData.Column { + if strings.ToLower(header) != "id" { + columns = append(columns, header) + } + } + return columns, nil +} + +// DeleteData deletes the specified data +func (s *LocalDataApp) DeleteData(uuid string) error { + instance, err := s.LocalDataRepo.GetByUUID(uuid) + if err != nil { + return err + } + localData := instance.(*entity.LocalData) + localData.Repo = s.LocalDataRepo + + site := entity.Site{ + Repo: s.SiteRepo, + } + if err := site.Load(); err != nil { + return errors.Wrapf(err, "failed to load connection info of FATE flow") + } + + dataListInstance, err := s.ProjectDataRepo.GetListByDataUUID(uuid) + if err != nil { + return errors.Wrap(err, "failed to query project data association") + } + dataList := dataListInstance.([]entity.ProjectData) + + var projectNameList []string + for _, projectData := range dataList { + if projectData.Status == entity.ProjectDataStatusAssociated { + projectInstance, err := s.ProjectRepo.GetByUUID(projectData.ProjectUUID) + if err != nil { + if err == repo.ErrProjectNotFound { + continue + } + return errors.Wrapf(err, "failed to query project %s", projectData.ProjectUUID) + } + project := projectInstance.(*entity.Project) + if project.Status == entity.ProjectStatusManaged || project.Status == entity.ProjectStatusJoined { + projectNameList = append(projectNameList, project.Name) + } + } + } + if len(projectNameList) > 0 { + return errors.Errorf("data is used by project(s): %v; dimiss the association before deleting the data", + projectNameList) + } + + localData.UploadContext = entity.UploadContext{ + FATEFlowHost: site.FATEFlowHost, + FATEFlowPort: site.FATEFlowHTTPPort, + FATEFlowIsHttps: false, + } + return localData.Destroy() +} + +func (s *LocalDataApp) UpdateIDMetaInfo(uuid string, req *LocalDataIDMetaInfoUpdateRequest) error { + return s.LocalDataRepo.UpdateIDMetaInfoByUUID(uuid, req.IDMetaInfo) +} diff --git a/site-portal/server/application/service/model_service.go b/site-portal/server/application/service/model_service.go new file mode 100644 index 00000000..b77bd310 --- /dev/null +++ b/site-portal/server/application/service/model_service.go @@ -0,0 +1,202 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" +) + +// ModelApp provides interfaces for model management APIs +type ModelApp struct { + ModelRepo repo.ModelRepository + ModelDeploymentRepo repo.ModelDeploymentRepository + SiteRepo repo.SiteRepository + ProjectRepo repo.ProjectRepository +} + +// ModelInfoBase contains the basic info of a model +type ModelInfoBase struct { + Name string `json:"name"` + UUID string `json:"uuid"` + ModelID string `json:"model_id"` + ModelVersion string `json:"model_version"` + ComponentName string `json:"component_name"` + CreateTime time.Time `json:"create_time"` + ProjectUUID string `json:"project_uuid"` + JobUUID string `json:"job_uuid"` + JobName string `json:"job_name"` + Role string `json:"role"` + PartyID uint `json:"party_id"` +} + +// ModelListItem contains info necessary to show models in a list +type ModelListItem struct { + ModelInfoBase + ProjectName string `json:"project_name"` + ComponentName string `json:"component_name"` +} + +// ModelDetail adds the evaluation info +type ModelDetail struct { + ModelListItem + Evaluation valueobject.ModelEvaluation `json:"evaluation"` +} + +// ModelCreationRequest is the request struct for creating a model +type ModelCreationRequest struct { + ModelInfoBase + Evaluation valueobject.ModelEvaluation `json:"evaluation"` + ComponentAlgorithmType entity.ComponentAlgorithmType `json:"algorithm_type"` +} + +// List returns model list of the current site or of the specified project +func (app *ModelApp) List(projectUUID string) ([]ModelListItem, error) { + var modelList []ModelListItem + queryFunc := app.ModelRepo.GetAll + if projectUUID != "" { + queryFunc = func() (interface{}, error) { + return app.ModelRepo.GetListByProjectUUID(projectUUID) + } + } + modelEntityListInstance, err := queryFunc() + if err != nil { + return nil, err + } + modelEntityList := modelEntityListInstance.([]entity.Model) + for _, modelEntity := range modelEntityList { + modelList = append(modelList, ModelListItem{ + ModelInfoBase: ModelInfoBase{ + Name: modelEntity.Name, + UUID: modelEntity.UUID, + ModelID: modelEntity.FATEModelID, + ModelVersion: modelEntity.FATEModelVersion, + CreateTime: modelEntity.CreatedAt, + ProjectUUID: modelEntity.ProjectUUID, + JobUUID: modelEntity.JobUUID, + JobName: modelEntity.JobName, + Role: modelEntity.Role, + PartyID: modelEntity.PartyID, + }, + ProjectName: modelEntity.ProjectName, + ComponentName: modelEntity.ComponentName, + }) + } + return modelList, nil +} + +// Delete deletes the specified model +func (app *ModelApp) Delete(modelUUID string) error { + return app.ModelRepo.DeleteByUUID(modelUUID) +} + +// Get returns detailed info of a model +func (app *ModelApp) Get(modelUUID string) (*ModelDetail, error) { + modelEntityInstance, err := app.ModelRepo.GetByUUID(modelUUID) + if err != nil { + return nil, err + } + modelEntity := modelEntityInstance.(*entity.Model) + + return &ModelDetail{ + ModelListItem: ModelListItem{ + ModelInfoBase: ModelInfoBase{ + Name: modelEntity.Name, + UUID: modelEntity.UUID, + ModelID: modelEntity.FATEModelID, + ModelVersion: modelEntity.FATEModelVersion, + CreateTime: modelEntity.CreatedAt, + ProjectUUID: modelEntity.ProjectUUID, + JobUUID: modelEntity.JobUUID, + JobName: modelEntity.JobName, + Role: modelEntity.Role, + PartyID: modelEntity.PartyID, + }, + ProjectName: modelEntity.ProjectName, + ComponentName: modelEntity.ComponentName, + }, + Evaluation: modelEntity.Evaluation, + }, nil +} + +// Create creates the model +func (app *ModelApp) Create(request *ModelCreationRequest) error { + projectInstance, err := app.ProjectRepo.GetByUUID(request.ProjectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + modelEntity := &entity.Model{ + Name: request.Name, + FATEModelID: request.ModelID, + FATEModelVersion: request.ModelVersion, + ProjectUUID: request.ProjectUUID, + ProjectName: project.Name, + JobUUID: request.JobUUID, + JobName: request.JobName, + ComponentName: request.ComponentName, + ComponentAlgorithmType: request.ComponentAlgorithmType, + Role: request.Role, + PartyID: request.PartyID, + Evaluation: request.Evaluation, + Repo: app.ModelRepo, + } + return modelEntity.Create() +} + +// Publish publishes the model to an online serving system +func (app *ModelApp) Publish(request *service.ModelDeploymentRequest) (*entity.ModelDeployment, error) { + site, err := app.loadSite() + if err != nil { + return nil, err + } + request.KubeflowConfig = site.KubeflowConfig + request.FATEFlowContext = entity.FATEFlowContext{ + FATEFlowHost: site.FATEFlowHost, + FATEFlowPort: site.FATEFlowHTTPPort, + FATEFlowIsHttps: false, + } + + domainService := service.ModelService{ + ModelRepo: app.ModelRepo, + ModelDeploymentRepo: app.ModelDeploymentRepo, + } + return domainService.DeployModel(request) +} + +// GetSupportedDeploymentTypes gets the supported deployment types this model can use +func (app *ModelApp) GetSupportedDeploymentTypes(modelUUID string) ([]entity.ModelDeploymentType, error) { + domainService := service.ModelService{ + ModelRepo: app.ModelRepo, + ModelDeploymentRepo: app.ModelDeploymentRepo, + } + return domainService.GetSupportedDeploymentType(modelUUID) +} + +// loadSite is a helper function to return site entity object +func (app *ModelApp) loadSite() (*entity.Site, error) { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return nil, errors.Wrapf(err, "failed to load site info") + } + return site, nil +} diff --git a/site-portal/server/application/service/project_service.go b/site-portal/server/application/service/project_service.go new file mode 100644 index 00000000..1f61f181 --- /dev/null +++ b/site-portal/server/application/service/project_service.go @@ -0,0 +1,1109 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/aggregate" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// ProjectApp provides interfaces for project management related APIs +type ProjectApp struct { + ProjectRepo repo.ProjectRepository + ParticipantRepo repo.ProjectParticipantRepository + SiteRepo repo.SiteRepository + InvitationRepo repo.ProjectInvitationRepository + ProjectDataRepo repo.ProjectDataRepository + LocalDataRepo repo.LocalDataRepository + JobApp *JobApp + ProjectSyncService *service.ProjectSyncService +} + +// ProjectCreationRequest is the request for creating a new local project +type ProjectCreationRequest struct { + Name string `json:"name"` + Description string `json:"description"` + AutoApprovalEnabled bool `json:"auto_approval_enabled"` +} + +// ProjectInfo is the detailed project info +type ProjectInfo struct { + ProjectListItemBase + AutoApprovalEnabled bool `json:"auto_approval_enabled"` +} + +// ProjectListItem contains basic info of a project plus data & job statistics +type ProjectListItem struct { + ProjectListItemBase + ParticipantsNum int64 `json:"participants_num"` + LocalDataNum int64 `json:"local_data_num"` + RemoteDataNum int64 `json:"remote_data_num"` + RunningJobNum int64 `json:"running_job_num"` + SuccessJobNum int64 `json:"success_job_num"` + PendingJobExist bool `json:"pending_job_exist"` +} + +// ProjectListItemBase contains basic info of a project +type ProjectListItemBase struct { + Name string `json:"name"` + Description string `json:"description"` + UUID string `json:"uuid"` + CreationTime time.Time `json:"creation_time"` + Manager string `json:"manager"` + ManagingSiteName string `json:"managing_site_name"` + ManagingSitePartyID uint `json:"managing_site_party_id"` + ManagedByThisSite bool `json:"managed_by_this_site"` +} + +// ProjectListItemClosed is a closed project +type ProjectListItemClosed struct { + ProjectListItemBase + ClosingStatus string `json:"closing_status"` +} + +// ProjectList contains joined projects and pending projects +type ProjectList struct { + JoinedProject []ProjectListItem `json:"joined_projects"` + PendingProject []ProjectListItemBase `json:"invited_projects"` + ClosedProject []ProjectListItemClosed `json:"closed_projects"` +} + +// ProjectParticipant contains info of a project participant +type ProjectParticipant struct { + ProjectParticipantBase + CreationTime time.Time `json:"creation_time"` + Status entity.ProjectParticipantStatus `json:"status"` + IsCurrentSite bool `json:"is_current_site"` +} + +// ProjectParticipantBase contains the basic info of a participant +type ProjectParticipantBase struct { + UUID string `json:"uuid"` + PartyID uint `json:"party_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// ProjectAutoApprovalStatus is a container for holding the auto-approval status value +type ProjectAutoApprovalStatus struct { + Enabled bool `json:"enabled"` +} + +// ProjectInvitationRequest is the request a site received for joining a project +type ProjectInvitationRequest struct { + UUID string `json:"uuid"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` +} + +// ProjectData contains information of an associated project data +type ProjectData struct { + Name string `json:"name"` + Description string `json:"description"` + DataID string `json:"data_id"` + CreationTime time.Time `json:"creation_time"` + UpdatedTime time.Time `json:"update_time"` + ProvidingSiteUUID string `json:"providing_site_uuid"` + ProvidingSiteName string `json:"providing_site_name"` + ProvidingSitePartyID uint `json:"providing_site_party_id"` + IsLocal bool `json:"is_local"` +} + +// ProjectDataAssociationRequest is the request to associate a local data to a project +type ProjectDataAssociationRequest struct { + Name string `json:"name"` + DataUUID string `json:"data_id"` +} + +// ProjectResourceSyncRequest is the request to sync certain project resource +type ProjectResourceSyncRequest struct { + ProjectUUID string `json:"project_uuid"` +} + +// CreateLocalProject creates a project locally +func (app *ProjectApp) CreateLocalProject(req *ProjectCreationRequest, username string) error { + project := &entity.Project{ + Name: req.Name, + Description: req.Description, + AutoApprovalEnabled: req.AutoApprovalEnabled, + Type: entity.ProjectTypeLocal, + ProjectCreatorInfo: valueobject.ProjectCreatorInfo{}, + Repo: app.ProjectRepo, + } + site := entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return errors.Wrapf(err, "failed to load site info") + } + creatorInfo := valueobject.ProjectCreatorInfo{ + Manager: username, + ManagingSiteName: site.Name, + ManagingSitePartyID: site.PartyID, + ManagingSiteUUID: site.UUID, + } + project.ProjectCreatorInfo = creatorInfo + if err := project.Create(); err != nil { + return err + } + participant := &entity.ProjectParticipant{ + UUID: uuid.NewV4().String(), + ProjectUUID: project.UUID, + SiteUUID: site.UUID, + SiteName: site.Name, + SitePartyID: site.PartyID, + SiteDescription: site.Description, + Status: entity.ProjectParticipantStatusOwner, + } + return app.ParticipantRepo.Create(participant) +} + +// List returns all projects this site joined or pending on this site +func (app *ProjectApp) List() (*ProjectList, error) { + if err := app.ProjectSyncService.EnsureProjectListSynced(); err != nil { + log.Err(err).Msg("failed to sync project list") + } + currentSite, err := app.LoadSite() + if err != nil { + return nil, err + } + instanceList, err := app.ProjectRepo.GetAll() + if err != nil { + return nil, err + } + projectList := &ProjectList{ + JoinedProject: make([]ProjectListItem, 0), + PendingProject: make([]ProjectListItemBase, 0), + } + domainProjectList := instanceList.([]entity.Project) + for _, project := range domainProjectList { + projectItemBase := ProjectListItemBase{ + Name: project.Name, + Description: project.Description, + UUID: project.UUID, + CreationTime: project.CreatedAt, + Manager: project.Manager, + ManagingSiteName: project.ManagingSiteName, + ManagingSitePartyID: project.ManagingSitePartyID, + ManagedByThisSite: project.Status == entity.ProjectStatusManaged, + } + switch project.Status { + case entity.ProjectStatusManaged, entity.ProjectStatusJoined: + projectAggregate := aggregate.ProjectAggregate{ + Project: &project, + Participant: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + participantsNum, err := projectAggregate.CountParticipant() + if err != nil { + return nil, err + } + + joinedProjectListItem := ProjectListItem{ + ProjectListItemBase: projectItemBase, + ParticipantsNum: participantsNum, + LocalDataNum: 0, + RemoteDataNum: 0, + RunningJobNum: 0, + SuccessJobNum: 0, + PendingJobExist: false, + } + + associatedDataList, err := app.ListData(joinedProjectListItem.UUID, "") + if err != nil { + return nil, err + } + for _, data := range associatedDataList { + if data.ProvidingSiteUUID == currentSite.UUID { + joinedProjectListItem.LocalDataNum++ + } else { + joinedProjectListItem.RemoteDataNum++ + } + } + + // XXX: this should be placed in the job context? + jobList, err := app.JobApp.List(joinedProjectListItem.UUID) + if err != nil { + return nil, err + } + for _, job := range jobList { + if job.PendingOnThisSite { + joinedProjectListItem.PendingJobExist = true + } + if job.Status == entity.JobStatusRunning { + joinedProjectListItem.RunningJobNum++ + } else if job.Status == entity.JobStatusSucceeded { + joinedProjectListItem.SuccessJobNum++ + } + } + + projectList.JoinedProject = append(projectList.JoinedProject, joinedProjectListItem) + case entity.ProjectStatusPending: + projectList.PendingProject = append(projectList.PendingProject, projectItemBase) + case entity.ProjectStatusClosed, entity.ProjectStatusDismissed, entity.ProjectStatusLeft: + closingStatusStr := func(status entity.ProjectStatus) string { + switch status { + case entity.ProjectStatusClosed: + return "closed by the managing site" + case entity.ProjectStatusLeft: + return "left" + case entity.ProjectStatusDismissed: + return "dismissed by the managing site" + default: + return "unknown" + } + }(project.Status) + projectList.ClosedProject = append(projectList.ClosedProject, ProjectListItemClosed{ + ProjectListItemBase: projectItemBase, + ClosingStatus: closingStatusStr, + }) + } + } + + joinedProjects := map[string]interface{}{} + for _, project := range projectList.JoinedProject { + joinedProjects[project.UUID] = nil + } + _ = app.ProjectSyncService.CleanupProject(joinedProjects) + + return projectList, nil +} + +// ListParticipant returns participants of a site or all participant registered in FML manager +func (app *ProjectApp) ListParticipant(uuid string, all bool) ([]ProjectParticipant, error) { + if err := app.ProjectSyncService.EnsureProjectParticipantSynced(uuid); err != nil { + log.Err(err).Msg("failed to sync project participant") + } + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return nil, err + } + project := &entity.Project{ + UUID: uuid, + Repo: app.ProjectRepo, + } + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + participants, err := projectAggregate.ListParticipant(all, &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }) + if err != nil { + return nil, err + } + list := make([]ProjectParticipant, len(participants)) + for i, participant := range participants { + list[i] = ProjectParticipant{ + ProjectParticipantBase: ProjectParticipantBase{ + UUID: participant.SiteUUID, + PartyID: participant.SitePartyID, + Name: participant.SiteName, + Description: participant.SiteDescription, + }, + CreationTime: participant.CreatedAt, + Status: participant.Status, + IsCurrentSite: participant.SiteUUID == site.UUID, + } + } + return list, nil +} + +// InviteParticipant invites certain participant to join current project +func (app *ProjectApp) InviteParticipant(uuid string, targetSite *ProjectParticipantBase) error { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return err + } + if err := app.EnsureProjectIsOpen(uuid); err != nil { + return err + } + projectInstance, err := app.ProjectRepo.GetByUUID(uuid) + if err != nil { + return err + } + project := projectInstance.(*entity.Project) + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.InviteParticipant(&aggregate.ProjectInvitationContext{ + FMLManagerConnectionInfo: &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + SitePartyID: targetSite.PartyID, + SiteUUID: targetSite.UUID, + SiteName: targetSite.Name, + SiteDescription: targetSite.Description, + }) +} + +// ProcessInvitation processes the invitation from FML manager +func (app *ProjectApp) ProcessInvitation(req *ProjectInvitationRequest) error { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return err + } + project := &entity.Project{ + UUID: req.ProjectUUID, + Name: req.ProjectName, + Description: req.ProjectDescription, + AutoApprovalEnabled: req.ProjectAutoApprovalEnabled, + ProjectCreatorInfo: valueobject.ProjectCreatorInfo{ + Manager: req.ProjectManager, + ManagingSiteName: req.ProjectManagingSiteName, + ManagingSitePartyID: req.ProjectManagingSitePartyID, + ManagingSiteUUID: req.ProjectManagingSiteUUID, + }, + Repo: app.ProjectRepo, + Model: gorm.Model{CreatedAt: req.ProjectCreationTime}, + } + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + invitation := &entity.ProjectInvitation{ + UUID: req.UUID, + ProjectUUID: req.ProjectUUID, + SiteUUID: req.SiteUUID, + } + return projectAggregate.ProcessInvitation(invitation, site.UUID) +} + +// ToggleAutoApprovalStatus changes the project's auto-approval status +func (app *ProjectApp) ToggleAutoApprovalStatus(uuid string, status *ProjectAutoApprovalStatus) error { + if err := app.EnsureProjectIsOpen(uuid); err != nil { + return err + } + project := &entity.Project{ + UUID: uuid, + AutoApprovalEnabled: status.Enabled, + } + return app.ProjectRepo.UpdateAutoApprovalStatusByUUID(project) +} + +// GetProject returns detailed info of a project +func (app *ProjectApp) GetProject(uuid string) (*ProjectInfo, error) { + projectInstance, err := app.ProjectRepo.GetByUUID(uuid) + if err != nil { + return nil, err + } + project := projectInstance.(*entity.Project) + return &ProjectInfo{ + ProjectListItemBase: ProjectListItemBase{ + Name: project.Name, + Description: project.Description, + UUID: project.UUID, + CreationTime: project.CreatedAt, + Manager: project.Manager, + ManagingSiteName: project.ManagingSiteName, + ManagingSitePartyID: project.ManagingSitePartyID, + ManagedByThisSite: project.Status == entity.ProjectStatusManaged, + }, + AutoApprovalEnabled: project.AutoApprovalEnabled, + }, nil +} + +// JoinOrRejectProject joins or refuses to join a pending project +func (app *ProjectApp) JoinOrRejectProject(uuid string, join bool) error { + site, err := app.LoadSite() + if err != nil { + return err + } + projectInstance, err := app.ProjectRepo.GetByUUID(uuid) + if err != nil { + return err + } + project := projectInstance.(*entity.Project) + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + if join { + return projectAggregate.JoinProject(fmlManagerConnectionInfo) + } else { + return projectAggregate.RejectProject(fmlManagerConnectionInfo) + } +} + +// LeaveProject removes the current site from the specified project +func (app *ProjectApp) LeaveProject(projectUUID string) error { + site, err := app.LoadSite() + if err != nil { + return err + } + + if err := app.ensureNoRunningJobs(projectUUID, site.UUID); err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + participantInstance, err := app.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, site.UUID) + if err != nil { + return errors.Wrap(err, "failed to query participant") + } + participant := participantInstance.(*entity.ProjectParticipant) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: participant, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + + return projectAggregate.LeaveProject(fmlManagerConnectionInfo) +} + +// CloseProject closes the managed project +func (app *ProjectApp) CloseProject(projectUUID string) error { + if err := app.ensureNoRunningJobs(projectUUID, ""); err != nil { + return err + } + + site, err := app.LoadSite() + if err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + return projectAggregate.CloseProject(fmlManagerConnectionInfo) +} + +// ProcessInvitationResponse handles the invitation response +func (app *ProjectApp) ProcessInvitationResponse(uuid string, accepted bool) error { + domainService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + if accepted { + return domainService.ProcessProjectAcceptance(uuid) + } else { + return domainService.ProcessProjectRejection(uuid) + } +} + +// ProcessInvitationRevocation handles the invitation revocation +func (app *ProjectApp) ProcessInvitationRevocation(uuid string) error { + domainService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + return domainService.ProcessInvitationRevocation(uuid) +} + +// CreateRemoteProjectParticipants processes a list of participants to create from FML manager for a remote project +func (app *ProjectApp) CreateRemoteProjectParticipants(projectUUID string, participants []entity.ProjectParticipant) error { + projectAggregate := aggregate.ProjectAggregate{ + Project: &entity.Project{ + UUID: projectUUID, + Repo: app.ProjectRepo, + }, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.CreateRemoteProjectParticipants(participants) +} + +// LoadSite is a helper function to return site entity object +func (app *ProjectApp) LoadSite() (*entity.Site, error) { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return nil, errors.Wrapf(err, "failed to load site info") + } + return site, nil +} + +// RemoveProjectParticipants removes joined participant or revoke invitation +func (app *ProjectApp) RemoveProjectParticipants(projectUUID string, siteUUID string) error { + site, err := app.LoadSite() + if err != nil { + return err + } + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrapf(err, "failed to get project") + } + project := projectInstance.(*entity.Project) + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + + participantInstance, err := app.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to query participant info") + } + participant := participantInstance.(*entity.ProjectParticipant) + if participant.Status == entity.ProjectParticipantStatusJoined { + if err := app.ensureNoRunningJobs(projectUUID, siteUUID); err != nil { + return err + } + } + return projectAggregate.RemoveParticipant(siteUUID, fmlManagerConnectionInfo) +} + +// ProcessParticipantInfoUpdate processes participant info update event by updating impacted repo records +func (app *ProjectApp) ProcessParticipantInfoUpdate(participant *ProjectParticipantBase) error { + toUpdateProjectTemplate := &entity.Project{ + ProjectCreatorInfo: valueobject.ProjectCreatorInfo{ + ManagingSiteName: participant.Name, + ManagingSitePartyID: participant.PartyID, + ManagingSiteUUID: participant.UUID, + }, + } + if err := app.ProjectRepo.UpdateManagingSiteInfoBySiteUUID(toUpdateProjectTemplate); err != nil { + return errors.Wrapf(err, "failed to update projects creator info") + } + toUpdateParticipantTemplate := &entity.ProjectParticipant{ + SiteUUID: participant.UUID, + SiteName: participant.Name, + SitePartyID: participant.PartyID, + SiteDescription: participant.Description, + } + if err := app.ParticipantRepo.UpdateParticipantInfoBySiteUUID(toUpdateParticipantTemplate); err != nil { + return errors.Wrapf(err, "failed to update projects participants info") + } + toUpdateDataTemplate := &entity.ProjectData{ + SiteUUID: participant.UUID, + SiteName: participant.Name, + SitePartyID: participant.PartyID, + } + if err := app.ProjectDataRepo.UpdateSiteInfoBySiteUUID(toUpdateDataTemplate); err != nil { + return errors.Wrapf(err, "failed to update projects data info") + } + return nil +} + +// ProcessParticipantLeaving processes participant leaving event by updating the repo record +func (app *ProjectApp) ProcessParticipantLeaving(projectUUID string, siteUUID string) error { + participantInstance, err := app.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, siteUUID) + if err != nil { + return err + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusLeft + if err := app.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return err + } + return nil +} + +// ProcessParticipantDismissal processes participant dismissal event +func (app *ProjectApp) ProcessParticipantDismissal(projectUUID string, siteUUID string) error { + if err := app.ensureNoRunningJobs(projectUUID, siteUUID); err != nil { + return err + } + site, err := app.LoadSite() + if err != nil { + return err + } + domainService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + return domainService.ProcessParticipantDismissal(projectUUID, siteUUID, site.UUID == siteUUID) +} + +// ListLocalData returns a list of local data that haven't been associated into the specified project +func (app *ProjectApp) ListLocalData(projectUUID string) ([]ProjectData, error) { + site, err := app.LoadSite() + if err != nil { + return nil, err + } + + instanceList, err := app.LocalDataRepo.GetAll() + if err != nil { + return nil, errors.Wrap(err, "failed to load local data list") + } + localDataList := instanceList.([]entity.LocalData) + + dataListInstance, err := app.ProjectDataRepo.GetListByProjectUUID(projectUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query project data") + } + associatedDataList := dataListInstance.([]entity.ProjectData) + + associatedDataUUIDMap := map[string]interface{}{} + for _, associatedData := range associatedDataList { + if associatedData.Status != entity.ProjectDataStatusAssociated { + continue + } + associatedDataUUIDMap[associatedData.DataUUID] = nil + } + + availableDataList := make([]ProjectData, 0) + for _, localData := range localDataList { + if _, ok := associatedDataUUIDMap[localData.UUID]; !ok && localData.JobStatus == entity.UploadJobStatusSucceeded { + availableDataList = append(availableDataList, ProjectData{ + Name: localData.Name, + Description: localData.Description, + DataID: localData.UUID, + CreationTime: localData.CreatedAt, + UpdatedTime: localData.UpdatedAt, + ProvidingSiteUUID: site.UUID, + ProvidingSiteName: site.Name, + ProvidingSitePartyID: site.PartyID, + IsLocal: true, + }) + } + } + return availableDataList, nil +} + +// ListData returns a list of data, local and remote, of the specified project +func (app *ProjectApp) ListData(projectUUID, participantUUID string) ([]ProjectData, error) { + if err := app.ProjectSyncService.EnsureProjectDataSynced(projectUUID); err != nil { + log.Err(err).Msg("failed to sync project data") + } + site, err := app.LoadSite() + if err != nil { + return nil, err + } + + var dataListInstance interface{} + if participantUUID == "" { + dataListInstance, err = app.ProjectDataRepo.GetListByProjectUUID(projectUUID) + } else { + dataListInstance, err = app.ProjectDataRepo.GetListByProjectAndSiteUUID(projectUUID, participantUUID) + } + if err != nil { + return nil, errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + + projectDataList := make([]ProjectData, 0) + for _, data := range dataList { + if data.Status != entity.ProjectDataStatusAssociated { + continue + } + projectDataList = append(projectDataList, ProjectData{ + Name: data.Name, + Description: data.Description, + DataID: data.DataUUID, + CreationTime: data.CreationTime, + UpdatedTime: data.UpdatedAt, + ProvidingSiteUUID: data.SiteUUID, + ProvidingSiteName: data.SiteName, + ProvidingSitePartyID: data.SitePartyID, + IsLocal: data.SiteUUID == site.UUID, + }) + } + + return projectDataList, nil +} + +// CreateDataAssociation associates local data into the specified project +func (app *ProjectApp) CreateDataAssociation(projectUUID string, request *ProjectDataAssociationRequest) error { + if err := app.EnsureProjectIsOpen(projectUUID); err != nil { + return err + } + dataInstance, err := app.LocalDataRepo.GetByUUID(request.DataUUID) + if err != nil { + return errors.Wrap(err, "failed to query local data") + } + localData := dataInstance.(*entity.LocalData) + + site, err := app.LoadSite() + if err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + return projectAggregate.AssociateLocalData(&aggregate.ProjectLocalDataAssociationContext{ + FMLManagerConnectionInfo: fmlManagerConnectionInfo, + LocalData: &entity.ProjectData{ + Name: localData.Name, + Description: localData.Description, + ProjectUUID: project.UUID, + DataUUID: localData.UUID, + SiteUUID: site.UUID, + SiteName: site.Name, + SitePartyID: site.PartyID, + Type: entity.ProjectDataTypeLocal, + Status: entity.ProjectDataStatusAssociated, + TableName: localData.TableName, + TableNamespace: localData.TableNamespace, + CreationTime: localData.CreatedAt, + UpdateTime: localData.UpdatedAt, + Repo: app.ProjectDataRepo, + }, + }) +} + +// RemoveDataAssociation dismisses the local data association +func (app *ProjectApp) RemoveDataAssociation(projectUUID, dataUUID string) error { + if err := app.EnsureProjectIsOpen(projectUUID); err != nil { + return err + } + site, err := app.LoadSite() + if err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + projectDataInstance, err := app.ProjectDataRepo.GetByProjectAndDataUUID(projectUUID, dataUUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + projectData := projectDataInstance.(*entity.ProjectData) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: projectData, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + fmlManagerConnectionInfo := &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + } + return projectAggregate.DismissAssociatedLocalData(&aggregate.ProjectLocalDataDismissalContext{ + FMLManagerConnectionInfo: fmlManagerConnectionInfo, + }) +} + +// CreateRemoteProjectDataAssociation adds the passed remote data association to the specified project +func (app *ProjectApp) CreateRemoteProjectDataAssociation(projectUUID string, dataList []entity.ProjectData) error { + site, err := app.LoadSite() + if err != nil { + return err + } + projectAggregate := aggregate.ProjectAggregate{ + Project: &entity.Project{ + UUID: projectUUID, + Repo: app.ProjectRepo, + }, + Participant: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.CreateRemoteProjectData(&aggregate.ProjectRemoteDataAssociationContext{ + LocalSiteUUID: site.UUID, + RemoteDataList: dataList, + }) +} + +// DismissRemoteProjectDataAssociation removes the data from the specified project +func (app *ProjectApp) DismissRemoteProjectDataAssociation(projectUUID string, dataUUIDList []string) error { + site, err := app.LoadSite() + if err != nil { + return err + } + projectAggregate := aggregate.ProjectAggregate{ + Project: &entity.Project{ + UUID: projectUUID, + Repo: app.ProjectRepo, + }, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.DeleteRemoteProjectData(&aggregate.ProjectRemoteDataDismissalContext{ + LocalSiteUUID: site.UUID, + RemoteDataUUIDList: dataUUIDList, + }) +} + +// ProcessProjectClosing processes project closing event +func (app *ProjectApp) ProcessProjectClosing(projectUUID string) error { + if err := app.ensureNoRunningJobs(projectUUID, ""); err != nil { + return err + } + + domainService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + return domainService.ProcessProjectClosing(projectUUID) +} + +// SyncProjectParticipant sync participant status of a project from the fml manager +func (app *ProjectApp) SyncProjectParticipant(projectUUID string) error { + if err := app.EnsureProjectIsOpen(projectUUID); err != nil { + return err + } + site, err := app.LoadSite() + if err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.SyncParticipant(&aggregate.ProjectSyncContext{ + FMLManagerConnectionInfo: &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + LocalSiteUUID: site.UUID, + }) +} + +// SyncProjectData sync data association status of a project from the fml manager +func (app *ProjectApp) SyncProjectData(projectUUID string) error { + if err := app.EnsureProjectIsOpen(projectUUID); err != nil { + return err + } + site, err := app.LoadSite() + if err != nil { + return err + } + + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return errors.Wrap(err, "failed to query project") + } + project := projectInstance.(*entity.Project) + + projectAggregate := aggregate.ProjectAggregate{ + Project: project, + Participant: nil, + ProjectData: nil, + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + DataRepo: app.ProjectDataRepo, + } + return projectAggregate.SyncDataAssociation(&aggregate.ProjectSyncContext{ + FMLManagerConnectionInfo: &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + LocalSiteUUID: site.UUID, + }) +} + +// SyncProject sync remote projects related to current site +func (app *ProjectApp) SyncProject() error { + site, err := app.LoadSite() + if err != nil { + return err + } + domainService := service.ProjectService{ + ProjectRepo: app.ProjectRepo, + ParticipantRepo: app.ParticipantRepo, + InvitationRepo: app.InvitationRepo, + ProjectDataRepo: app.ProjectDataRepo, + } + return domainService.ProcessProjectSyncRequest(&aggregate.ProjectSyncContext{ + FMLManagerConnectionInfo: &aggregate.FMLManagerConnectionInfo{ + Connected: site.FMLManagerConnected, + Endpoint: site.FMLManagerEndpoint, + ServerName: site.FMLManagerServerName, + }, + LocalSiteUUID: site.UUID, + }) +} + +func (app *ProjectApp) ensureNoRunningJobs(projectUUID, siteUUID string) error { + // XXX: job checking logic should be placed in the job context? + jobList, err := app.JobApp.List(projectUUID) + if err != nil { + return err + } + for _, jobItem := range jobList { + if jobItem.Status != entity.JobStatusRejected && + jobItem.Status != entity.JobStatusFailed && + jobItem.Status != entity.JobStatusSucceeded && + jobItem.Status != entity.JobStatusDeleted && + jobItem.Status != entity.JobStatusUnknown { + // filter jobs by siteUUID + if siteUUID != "" { + _, err := app.JobApp.ParticipantRepo.GetByJobAndSiteUUID(jobItem.UUID, siteUUID) + if errors.Is(err, repo.ErrJobParticipantNotFound) { + continue + } + } + return errors.Errorf("at least one job is not in finished status - job: %s(%s)", jobItem.Name, jobItem.StatusStr) + } + } + return nil +} + +// EnsureProjectIsOpen returns error if the project is not in a "opened" status. +// This function can be used in other call-sites before start changing project resources. +func (app *ProjectApp) EnsureProjectIsOpen(projectUUID string) error { + projectInstance, err := app.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return err + } + project := projectInstance.(*entity.Project) + if project.Status == entity.ProjectStatusClosed || + project.Status == entity.ProjectStatusLeft || + project.Status == entity.ProjectStatusDismissed || + project.Status == entity.ProjectStatusRejected { + return errors.Errorf(`project can not be accessed in status: %v`, project.Status) + } + return nil +} diff --git a/site-portal/server/application/service/site_service.go b/site-portal/server/application/service/site_service.go new file mode 100644 index 00000000..50f2e84d --- /dev/null +++ b/site-portal/server/application/service/site_service.go @@ -0,0 +1,85 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" +) + +// SiteApp provide functions to manage the site +type SiteApp struct { + // SiteRepo is the repository for persisting site info + SiteRepo repo.SiteRepository +} + +// FATEFlowConnectionInfo represent connection info to a fate flow service +type FATEFlowConnectionInfo struct { + // Host address + Host string `json:"host"` + // Port is the port number + Port uint `json:"port"` + // Https is whether https is enabled + Https bool `json:"https"` +} + +// FMLManagerConnectionInfo contains connection settings for the fml manager +type FMLManagerConnectionInfo struct { + // Endpoint address starting with "http" or "https" + Endpoint string `json:"endpoint"` + //ServerName is used by Site Portal to verify FML Manager's certificate + ServerName string `json:"server_name"` +} + +// GetSite returns the site information +func (app *SiteApp) GetSite() (*entity.Site, error) { + site := &entity.Site{ + Repo: app.SiteRepo, + } + if err := site.Load(); err != nil { + return nil, err + } + return site, nil +} + +// UpdateSite updates the site info +func (app *SiteApp) UpdateSite(updatedSiteInfo *entity.Site) error { + site := &entity.Site{ + Repo: app.SiteRepo, + } + return site.UpdateConfigurableInfo(updatedSiteInfo) +} + +// TestFATEFlowConnection tests the connection to fate flow service +func (app *SiteApp) TestFATEFlowConnection(connectionInfo *FATEFlowConnectionInfo) error { + site := &entity.Site{ + Repo: app.SiteRepo, + } + return site.ConnectFATEFlow(connectionInfo.Host, connectionInfo.Port, connectionInfo.Https) +} + +// TestKubeflowConnection tests the connection to Kubernetes and if it has KFServing installed +func (app *SiteApp) TestKubeflowConnection(connectionInfo *valueobject.KubeflowConfig) error { + return connectionInfo.Validate() +} + +// RegisterToFMLManager connects to fml manager and register the current site +func (app *SiteApp) RegisterToFMLManager(connectionInfo *FMLManagerConnectionInfo) error { + site := &entity.Site{ + Repo: app.SiteRepo, + } + return site.RegisterToFMLManager(connectionInfo.Endpoint, connectionInfo.ServerName) +} diff --git a/site-portal/server/application/service/user_service.go b/site-portal/server/application/service/user_service.go new file mode 100644 index 00000000..a49ed94a --- /dev/null +++ b/site-portal/server/application/service/user_service.go @@ -0,0 +1,127 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/service" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "gorm.io/gorm" +) + +// UserApp provides user management service +type UserApp struct { + UserRepo repo.UserRepository +} + +// PublicUser represents a user info viewable to the public +type PublicUser struct { + Name string `json:"name"` + ID uint `json:"id"` + UUID string `json:"uuid"` + valueobject.UserPermissionInfo +} + +// LoginInfo represents fields related with login +type LoginInfo struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// PwdChangeInfo represents fields related with login +type PwdChangeInfo struct { + CurPassword string `json:"cur_password"` + NewPassword string `json:"new_password"` +} + +// GetUsers returns all the available users as a list of PublicUser +func (app *UserApp) GetUsers() ([]PublicUser, error) { + repoUsers, err := app.UserRepo.GetAllUsers() + if err != nil { + return nil, err + } + userList := repoUsers.([]entity.User) + users := make([]PublicUser, len(userList)) + for index, repoUser := range userList { + users[index] = PublicUser{ + Name: repoUser.Name, + ID: repoUser.ID, + UUID: repoUser.UUID, + UserPermissionInfo: repoUser.PermissionInfo, + } + } + return users, nil +} + +// UpdateUserPermission changes a user's valueobject.UserPermissionInfo +func (app *UserApp) UpdateUserPermission(publicUser *PublicUser) error { + user := &entity.User{ + Model: gorm.Model{ + ID: publicUser.ID, + }, + Repo: app.UserRepo, + } + if err := user.LoadById(); err != nil { + return err + } + return user.UpdatePermissionInfo(publicUser.UserPermissionInfo) +} + +// Login validates the loginInfo and returns a publicUser object on success +func (app *UserApp) Login(info *LoginInfo) (*PublicUser, error) { + loginService := &service.UserService{ + Repo: app.UserRepo, + } + user, err := loginService.LoginService(info.Username, info.Password) + if err != nil { + return nil, err + } + publicUser := PublicUser{ + Name: user.Name, + ID: user.ID, + UUID: user.UUID, + UserPermissionInfo: user.PermissionInfo, + } + return &publicUser, nil +} + +// CheckAccess validates if the user can access site portal +func (app *UserApp) CheckAccess(publicUser *PublicUser) error { + user := &entity.User{ + Model: gorm.Model{ + ID: publicUser.ID, + }, + Repo: app.UserRepo, + } + if err := user.LoadById(); err != nil { + return err + } + return user.CheckSitePortalAccess() +} + +// UpdateUserPassword changes a user's password +func (app *UserApp) UpdateUserPassword(userId int, info *PwdChangeInfo) error { + user := &entity.User{ + Model: gorm.Model{ + ID: uint(userId), + }, + Repo: app.UserRepo, + } + if err := user.LoadById(); err != nil { + return err + } + return user.UpdatePwdInfo(info.CurPassword, info.NewPassword) +} diff --git a/site-portal/server/constants/common.go b/site-portal/server/constants/common.go new file mode 100644 index 00000000..633a3847 --- /dev/null +++ b/site-portal/server/constants/common.go @@ -0,0 +1,233 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +const ( + APIVersion = "v1" + JobComponents = `[{ + "groupName": "Data Input and Output", + "modules": [{ + "moduleName": "DataIO", + "parameters": { + "input_format": { + "drop_down_box": ["dense", "sparse", "tag"] + }, + "delimitor": ",", + "data_type": { + "drop_down_box": ["float", "float64", "int", "int64", "str", "long"] + }, + "exclusive_data_type": {}, + "tag_with_value": false, + "tag_value_delimitor": ":", + "missing_fill": false, + "default_value": 0, + "missing_fill_method": "", + "missing_impute": [], + "outlier_replace": false, + "outlier_replace_method": "", + "outlier_impute": [], + "outlier_replace_value": [], + "with_label": true, + "label_name": "y", + "label_type": { + "drop_down_box": ["int", "int64", "float", "float64", "str", "long"] + }, + "output_format": "dense" + }, + "conditions": { + "possible_input": ["Reader", "DataIO"], + "can_be_endpoint": false + }, + "input": { + "data": ["data"], + "model": ["model"] + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + { + "moduleName": "HomoDataSplit", + "parameters": { + "random_state": "", + "test_size": 0.0, + "train_size": 0.8, + "validate_size": 0.2, + "stratified": false, + "shuffle": true, + "split_points": [], + "need_run": true + }, + "conditions": { + "possible_input": ["DataIO", "HomoOneHotEncoder"], + "can_be_endpoint": false + }, + "input": { + "data": ["data"], + "model": [] + }, + "output": { + "data": ["train_data", "validate_data", "test_data"], + "model": [] + } + } + ] + }, + { + "groupName": "Feature Engineering", + "modules": [{ + "moduleName": "HomoOneHotEncoder", + "parameters": { + "transform_col_indexes": -1, + "need_run": true, + "need_alignment": true + }, + "conditions": { + "possible_input": ["DataIO"], + "can_be_endpoint": false + }, + "input": { + "data": ["data"], + "model": ["model"] + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }] + }, + { + "groupName": "Homogeneous Algorithms", + "modules": [{ + "moduleName": "HomoLR", + "parameters": { + "penalty": { + "drop_down_box": ["L2", "L1", "None"] + }, + "tol": 1e-4, + "alpha": 1.0, + "optimizer": { + "drop_down_box": ["rmsprop", "sgd", "adam", "nesterov_momentum_sgd", "adagrad"] + }, + "batch_size": -1, + "learning_rate": 0.01, + "max_iter": 100, + "early_stop": { + "drop_down_box": ["diff", "weight_diff", "abs"] + }, + "decay": 1, + "decay_sqrt": true, + "encrypt_param": {}, + "predict_param": {}, + "cv_param": { + "n_splits": 4, + "shuffle": true, + "random_seed": 33, + "need_cv": false + }, + "multi_class": { + "drop_down_box": ["ovr"] + }, + "validation_freqs": "", + "early_stopping_rounds": "", + "metrics": "", + "use_first_metric_only": false, + "floating_point_precision": "" + }, + "conditions": { + "possible_input": ["DataIO", "HomoOneHotEncoder", "HomoDataSplit"], + "can_be_endpoint": true + }, + "input": { + "data": ["data", "train_data", "validate_data"], + "model": ["model"] + }, + "output": { + "data": ["data"], + "model": ["model"] + } + }, + { + "moduleName": "HomoSecureboost", + "parameters": { + "task_type": "classification", + "objective_param": { + "objective": "cross_entropy" + }, + "learning_rate": 0.3, + "num_trees": 5, + "subsample_feature_rate": 1.0, + "n_iter_no_change": true, + "bin_num": 32, + "validation_freqs": 1, + "tree_param": { + "max_depth": 3 + } + }, + "conditions": { + "possible_input": ["DataIO", "HomoOneHotEncoder", "HomoDataSplit"], + "can_be_endpoint": true + }, + "input": { + "data": ["data", "train_data", "validate_data"], + "model": ["model"] + }, + "output": { + "data": ["data"], + "model": ["model"] + } + } + ] + }, + { + "groupName": "Evaluation", + "modules": [{ + "moduleName": "Evaluation", + "parameters": { + "eval_type": { + "drop_down_box": ["binary", "regression"] + }, + "unfold_multi_result": false, + "pos_label": "1", + "need_run": true + }, + "conditions": { + "possible_input": ["HomoLR", "HomoSecureboost"], + "can_be_endpoint": true + }, + "input": { + "data": ["data"], + "model": [] + }, + "output": { + "data": ["data"], + "model": [] + } + }] + } +]` +) + +var ( + // Branch is the source branch + Branch string + + // Commit is the commit number + Commit string + + // BuildTime is the compiling time + BuildTime string +) diff --git a/site-portal/server/constants/response_code.go b/site-portal/server/constants/response_code.go new file mode 100644 index 00000000..bad13506 --- /dev/null +++ b/site-portal/server/constants/response_code.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package constants + +const ( + // RespNoErr means a success operation + RespNoErr = 0 + // RespInternalErr means some error occurred + RespInternalErr = 10001 +) diff --git a/site-portal/server/docs/docs.go b/site-portal/server/docs/docs.go new file mode 100644 index 00000000..e904ebc9 --- /dev/null +++ b/site-portal/server/docs/docs.go @@ -0,0 +1,5493 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "List all data records", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.LocalDataListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Upload a local csv data", + "parameters": [ + { + "type": "file", + "description": "The csv file", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Data name", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Data description", + "name": "description", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the data UUID", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Get data record's detailed info", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LocalDataDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Delete the data file, both the local copy and the FATE table", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/columns": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Get data headers", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/file": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Download data file", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/idmetainfo": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Update data record's ID meta info", + "parameters": [ + { + "description": "The meta info", + "name": "info", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LocalDataIDMetaInfoUpdateRequest" + } + }, + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/components": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get all the components and their default configs. The returned format is json", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/conf/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get a job's config template, used in json template mode", + "parameters": [ + { + "description": "Job requests, not all fields are required: only need to fill related ones according to job type", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobConf" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/generateConfFromDag": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Generate the conf json file from the DAG the user draw, the conf file can be consumed by Fateflow", + "parameters": [ + { + "description": "The request for generate the conf json file", + "name": "generateJobConfRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.GenerateJobConfRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/generateDslFromDag": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Generate the DSL json file from the DAG the user draw, should be called by UI only", + "parameters": [ + { + "description": "The raw json, the value should be a serialized json string", + "name": "rawJson", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobRawDagJson" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Create a new job that is created by other site, only called by FML manager", + "parameters": [ + { + "description": "Job info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RemoteJobCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/{uuid}/response": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Handle job response from other sites, only called by FML manager", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Approval context, containing the sender UUID and approval status", + "name": "context", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobApprovalContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/{uuid}/status": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Handle job status updates, only called by FML manager", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Job status update context, containing the latest job and participant status", + "name": "context", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobStatusUpdateContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/predict/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get allowed participants for a predicting job from a model", + "parameters": [ + { + "type": "string", + "description": "UUID of a trained model", + "name": "modelUUID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobParticipantInfoBase" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get job's detailed info", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Delete the job. The job will be marked as delete in this site, but still viewable in other sites", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/approve": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Approve a pending job", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/data-result/download": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "the result data of a Predicting or PSI job, XXX: currently it will return an error message due to a bug in FATE", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/refresh": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Refresh the latest job status", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Disapprove a pending job", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get model list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ModelListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/internal/event/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Handle model creation event, called by the job context only", + "parameters": [ + { + "description": "Creation Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ModelCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get model's detailed info", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ModelDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Delete the model", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}/publish": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Publish model to online serving system", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Creation Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ModelDeploymentRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.ModelDeployment" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}/supportedDeploymentTypes": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get list of deployment types (KFServing, FATE-Serving, etc.) this model can use", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all project", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ProjectList" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create a new project", + "parameters": [ + { + "description": "Basic project info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/data/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process data sync event, to sync the data association info from fml manager", + "parameters": [ + { + "description": "Info of the project to by synced", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectResourceSyncRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/list/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process list sync event, to sync the projects list status from fml manager", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/participant/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info sync event, to sync the participant info from fml manager", + "parameters": [ + { + "description": "Info of the project to by synced", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectResourceSyncRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/participant/update": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info update event, called by the FML manager only", + "parameters": [ + { + "description": "Updated participant info", + "name": "participant", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectParticipantBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project invitation, called by FML manager only", + "parameters": [ + { + "description": "invitation request", + "name": "invitation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectInvitationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/accept": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation acceptance response, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation rejection response, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/revoke": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation revocation request, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project closing event, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/data/associate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Add associated remote data to current project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Remote data information", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ProjectData" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/data/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Dismiss associated remote data from current project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data UUID list", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participant/{siteUUID}/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant dismissal event, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participant/{siteUUID}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant leaving request, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participants": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create joined participants for a project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "participants list", + "name": "participantList", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ProjectParticipant" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get project's detailed info", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ProjectInfo" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/autoapprovalstatus": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Change a project's auto-approval status", + "parameters": [ + { + "description": "The auto-approval status, only an 'enabled' field", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectAutoApprovalStatus" + } + }, + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Close the managed project, only available to project managing site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get associated data list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "participant uuid, i.e. the providing site uuid; if set, only returns the associated data of the specified participant", + "name": "participant", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectData" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Associate local data to current project", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Local data info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/local": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get available local data for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectData" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/{dataUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Remove associated data from the current project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Data UUID", + "name": "dataUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Invite other site to this project", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "target site information", + "name": "info", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectParticipantBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/job": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get job list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobListItemBase" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create a new job", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Job requests, only fill related field according to job type", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobListItemBase" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/join": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Join a pending/invited project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Leave the joined project created by other site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/model": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get model list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ModelListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List participants", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, returns all sites, including not joined ones", + "name": "all", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectParticipant" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{participantUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Remove pending participant (revoke invitation) or dismiss joined participant", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Participant UUID", + "name": "participantUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Reject to join a pending/invited project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Return site data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Site" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Update site information", + "parameters": [ + { + "description": "The site information, some info like id, UUID, connected status cannot be changed", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.Site" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/fateflow/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Test site connection to fate flow service", + "parameters": [ + { + "description": "The fate flow connection info", + "name": "connectInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FATEFlowConnectionInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/fmlmanager/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Connect to fml manager and register itself", + "parameters": [ + { + "description": "The FML Manager endpoint", + "name": "connectInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FMLManagerConnectionInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/kubeflow/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Test site connection to Kubeflow, including MinIO and KFServing", + "parameters": [ + { + "description": "The Kubeflow config info", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.KubeflowConfig" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List all saved users", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.PublicUser" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/current": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Return current user in the jwt token", + "responses": { + "200": { + "description": "Success, the name of current user", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "login to site portal", + "parameters": [ + { + "description": "credentials for login", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + } + } + } + }, + "/user/logout": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout from the site portal", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{id}/password": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user Password", + "parameters": [ + { + "description": "current and new password", + "name": "passwordChangeInfo", + "in": "body", + "schema": { + "$ref": "#/definitions/service.PwdChangeInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{id}/permission": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user permission", + "parameters": [ + { + "description": "Permission, must contain all permissions, otherwise the missing once will be considered as false", + "name": "permission", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.UserPermissionInfo" + } + }, + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.ModelDeployment": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "model_uuid": { + "type": "string" + }, + "parameters_json": { + "type": "string" + }, + "request_json": { + "type": "string" + }, + "result_json": { + "type": "string" + }, + "service_name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ProjectData": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "data_uuid": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "table_name": { + "type": "string" + }, + "table_namespace": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "update_time": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ProjectParticipant": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + }, + "site_description": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.Site": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "description": "Description contains more text about this site", + "type": "string" + }, + "external_host": { + "description": "ExternalHost is the IP or hostname this site portal service is exposed", + "type": "string" + }, + "external_port": { + "description": "ExternalPort the port number this site portal service is exposed", + "type": "integer" + }, + "fate_flow_connected": { + "description": "FATEFlowConnected is whether the portal has connected to FATEFlow after the address is configured", + "type": "boolean" + }, + "fate_flow_connected_at": { + "description": "FATEFlowConnectedAt is the last time this portal connected to the FATE flow", + "type": "string" + }, + "fate_flow_grpc_port": { + "description": "FATEFlowGRPCPort is the grpc port number of the FATE-flow service, currently not used", + "type": "integer" + }, + "fate_flow_host": { + "description": "FATEFlowHost is the host address of the FATE-flow service", + "type": "string" + }, + "fate_flow_http_port": { + "description": "FATEFlowHTTPPort is the http port number of the FATE-flow service", + "type": "integer" + }, + "fml_manager_connected": { + "description": "FMLManagerConnected is whether the portal is connected to FML manager", + "type": "boolean" + }, + "fml_manager_connected_at": { + "description": "FMLManagerConnectedAt is the last time this portal has registered to a FML manager", + "type": "string" + }, + "fml_manager_endpoint": { + "description": "FMLManagerEndpoint is of format \"\u003chttp or https\u003e://\u003chost\u003e:\u003cport\u003e\"", + "type": "string" + }, + "fml_manager_server_name": { + "description": "FMLManagerServerName is used to verify FML Manager's certificate", + "type": "string" + }, + "https": { + "description": "HTTPS choose if site portal has HTTPS enabled", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "kubeflow_config": { + "description": "KubeflowConfig records the Kubeflow related information for deploying horizontal model to the KFServing system", + "$ref": "#/definitions/valueobject.KubeflowConfig" + }, + "kubeflow_connected": { + "description": "KubeflowConnected is whether this site has connected to the Kubeflow since it is configured", + "type": "boolean" + }, + "kubeflow_connected_at": { + "description": "KubeflowConnectedAt is the last time this portal has successfully connected all related Kubeflow service", + "type": "string" + }, + "name": { + "description": "Name is the site's name", + "type": "string" + }, + "party_id": { + "description": "PartyID is the id of this party", + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "service.FATEFlowConnectionInfo": { + "type": "object", + "properties": { + "host": { + "description": "Host address", + "type": "string" + }, + "https": { + "description": "Https is whether https is enabled", + "type": "boolean" + }, + "port": { + "description": "Port is the port number", + "type": "integer" + } + } + }, + "service.FMLManagerConnectionInfo": { + "type": "object", + "properties": { + "endpoint": { + "description": "Endpoint address starting with \"http\" or \"https\"", + "type": "string" + }, + "server_name": { + "description": "ServerName is used by Site Portal to verify FML Manager's certificate", + "type": "string" + } + } + }, + "service.GenerateJobConfRequest": { + "type": "object", + "properties": { + "dag_json": { + "$ref": "#/definitions/service.JobRawDagJson" + }, + "job_conf": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + }, + "service.IntersectionJobResultInfo": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "header": { + "type": "array", + "items": { + "type": "string" + } + }, + "intersect_number": { + "type": "integer" + }, + "intersect_rate": { + "type": "number" + } + } + }, + "service.JobApprovalContext": { + "type": "object", + "properties": { + "approved": { + "type": "boolean" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobConf": { + "type": "object", + "properties": { + "conf_json": { + "type": "string" + }, + "dsl_json": { + "type": "string" + } + } + }, + "service.JobData": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_local": { + "type": "boolean" + }, + "label_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providing_site_name": { + "type": "string" + }, + "providing_site_party_id": { + "type": "integer" + }, + "providing_site_uuid": { + "type": "string" + }, + "site_status": { + "type": "integer" + }, + "site_status_str": { + "type": "string" + } + } + }, + "service.JobDataBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, + "service.JobDetail": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_name": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "initiating_site_name": { + "type": "string" + }, + "initiating_site_party_id": { + "type": "integer" + }, + "initiating_site_uuid": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobData" + }, + "is_initiator": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobData" + } + }, + "pending_on_this_site": { + "type": "boolean" + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "result_info": { + "$ref": "#/definitions/service.JobResultInfo" + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + }, + "status_str": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobListItemBase": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_name": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "initiating_site_name": { + "type": "string" + }, + "initiating_site_party_id": { + "type": "integer" + }, + "initiating_site_uuid": { + "type": "string" + }, + "is_initiator": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pending_on_this_site": { + "type": "boolean" + }, + "project_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "status_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobParticipantInfoBase": { + "type": "object", + "properties": { + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobRawDagJson": { + "type": "object", + "properties": { + "raw_json": { + "type": "string" + } + } + }, + "service.JobResultInfo": { + "type": "object", + "properties": { + "intersection_result": { + "$ref": "#/definitions/service.IntersectionJobResultInfo" + }, + "predicting_result": { + "$ref": "#/definitions/service.PredictingJobResultInfo" + }, + "training_result": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "service.JobStatusUpdateContext": { + "type": "object", + "properties": { + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_id": { + "type": "string" + }, + "fate_model_version": { + "type": "string" + }, + "participant_status_map": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + } + } + }, + "service.JobSubmissionRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.LocalDataDetail": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "feature_size": { + "type": "integer" + }, + "features_array": { + "type": "array", + "items": { + "type": "string" + } + }, + "filename": { + "type": "string" + }, + "id_meta_info": { + "$ref": "#/definitions/valueobject.IDMetaInfo" + }, + "name": { + "type": "string" + }, + "preview_array": { + "type": "string" + }, + "sample_size": { + "type": "integer" + }, + "table_name": { + "type": "string" + }, + "upload_job_status": { + "type": "integer" + } + } + }, + "service.LocalDataIDMetaInfoUpdateRequest": { + "type": "object", + "properties": { + "id_encryption_type": { + "type": "integer" + }, + "id_type": { + "type": "integer" + } + } + }, + "service.LocalDataListItem": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "feature_size": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sample_size": { + "type": "integer" + }, + "upload_job_status": { + "type": "integer" + } + } + }, + "service.LoginInfo": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.ModelCreationRequest": { + "type": "object", + "properties": { + "algorithm_type": { + "type": "integer" + }, + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "evaluation": { + "$ref": "#/definitions/valueobject.ModelEvaluation" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ModelDeploymentRequest": { + "type": "object", + "properties": { + "deployment_type": { + "type": "integer" + }, + "parameters_json": { + "type": "string" + }, + "service_name": { + "type": "string" + } + } + }, + "service.ModelDetail": { + "type": "object", + "properties": { + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "evaluation": { + "$ref": "#/definitions/valueobject.ModelEvaluation" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ModelListItem": { + "type": "object", + "properties": { + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PredictingJobResultInfo": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "header": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "service.ProjectAutoApprovalStatus": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "service.ProjectCreationRequest": { + "type": "object", + "properties": { + "auto_approval_enabled": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.ProjectData": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_local": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "providing_site_name": { + "type": "string" + }, + "providing_site_party_id": { + "type": "integer" + }, + "providing_site_uuid": { + "type": "string" + }, + "update_time": { + "type": "string" + } + } + }, + "service.ProjectDataAssociationRequest": { + "type": "object", + "properties": { + "data_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.ProjectInfo": { + "type": "object", + "properties": { + "auto_approval_enabled": { + "type": "boolean" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectInvitationRequest": { + "type": "object", + "properties": { + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectList": { + "type": "object", + "properties": { + "closed_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItemClosed" + } + }, + "invited_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItemBase" + } + }, + "joined_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItem" + } + } + } + }, + "service.ProjectListItem": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "local_data_num": { + "type": "integer" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "participants_num": { + "type": "integer" + }, + "pending_job_exist": { + "type": "boolean" + }, + "remote_data_num": { + "type": "integer" + }, + "running_job_num": { + "type": "integer" + }, + "success_job_num": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectListItemBase": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectListItemClosed": { + "type": "object", + "properties": { + "closing_status": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectParticipant": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_current_site": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectParticipantBase": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectResourceSyncRequest": { + "type": "object", + "properties": { + "project_uuid": { + "type": "string" + } + } + }, + "service.PublicUser": { + "type": "object", + "properties": { + "fateboard_access": { + "description": "FATEBoardAccess controls whether the user can access fate board", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "notebook_access": { + "description": "NoteBookAccess controls whether the user can access notebook", + "type": "boolean" + }, + "site_portal_access": { + "description": "SitePortalAccess controls whether the user can access site portal", + "type": "boolean" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PwdChangeInfo": { + "type": "object", + "properties": { + "cur_password": { + "type": "string" + }, + "new_password": { + "type": "string" + } + } + }, + "service.RemoteJobCreationRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "valueobject.IDMetaInfo": { + "type": "object", + "properties": { + "id_encryption_type": { + "type": "integer" + }, + "id_type": { + "type": "integer" + } + } + }, + "valueobject.KubeflowConfig": { + "type": "object", + "properties": { + "kubeconfig": { + "description": "KubeConfig is the content of the kubeconfig file to connect to kubernetes", + "type": "string" + }, + "minio_access_key": { + "description": "MinIOAccessKey is the access-key for the MinIO server", + "type": "string" + }, + "minio_endpoint": { + "description": "MinIOEndpoint is the address for the MinIO server", + "type": "string" + }, + "minio_region": { + "description": "MinIORegion is the region of the MinIO service", + "type": "string" + }, + "minio_secret_key": { + "description": "MinIOSecretKey is the secret-key for the MinIO server", + "type": "string" + }, + "minio_ssl_enabled": { + "description": "MinIOSSLEnabled is whether this connection should be over ssl", + "type": "boolean" + } + } + }, + "valueobject.ModelEvaluation": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "valueobject.UserPermissionInfo": { + "type": "object", + "properties": { + "fateboard_access": { + "description": "FATEBoardAccess controls whether the user can access fate board", + "type": "boolean" + }, + "notebook_access": { + "description": "NoteBookAccess controls whether the user can access notebook", + "type": "boolean" + }, + "site_portal_access": { + "description": "SitePortalAccess controls whether the user can access site portal", + "type": "boolean" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "site portal API service", + Description: "backend APIs of site portal service", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/site-portal/server/docs/swagger.json b/site-portal/server/docs/swagger.json new file mode 100644 index 00000000..08202827 --- /dev/null +++ b/site-portal/server/docs/swagger.json @@ -0,0 +1,5469 @@ +{ + "swagger": "2.0", + "info": { + "description": "backend APIs of site portal service", + "title": "site portal API service", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "FedLCM team" + }, + "version": "v1" + }, + "basePath": "/api/v1", + "paths": { + "/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "List all data records", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.LocalDataListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Upload a local csv data", + "parameters": [ + { + "type": "file", + "description": "The csv file", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Data name", + "name": "name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Data description", + "name": "description", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Success, the data field is the data UUID", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Get data record's detailed info", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LocalDataDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Delete the data file, both the local copy and the FATE table", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/columns": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Get data headers", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/file": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Download data file", + "parameters": [ + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/data/{uuid}/idmetainfo": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "LocalData" + ], + "summary": "Update data record's ID meta info", + "parameters": [ + { + "description": "The meta info", + "name": "info", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LocalDataIDMetaInfoUpdateRequest" + } + }, + { + "type": "string", + "description": "Data UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/components": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get all the components and their default configs. The returned format is json", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/conf/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get a job's config template, used in json template mode", + "parameters": [ + { + "description": "Job requests, not all fields are required: only need to fill related ones according to job type", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobConf" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/generateConfFromDag": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Generate the conf json file from the DAG the user draw, the conf file can be consumed by Fateflow", + "parameters": [ + { + "description": "The request for generate the conf json file", + "name": "generateJobConfRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.GenerateJobConfRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/generateDslFromDag": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Generate the DSL json file from the DAG the user draw, should be called by UI only", + "parameters": [ + { + "description": "The raw json, the value should be a serialized json string", + "name": "rawJson", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobRawDagJson" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Create a new job that is created by other site, only called by FML manager", + "parameters": [ + { + "description": "Job info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RemoteJobCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/{uuid}/response": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Handle job response from other sites, only called by FML manager", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Approval context, containing the sender UUID and approval status", + "name": "context", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobApprovalContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/internal/{uuid}/status": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Handle job status updates, only called by FML manager", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Job status update context, containing the latest job and participant status", + "name": "context", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobStatusUpdateContext" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/predict/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get allowed participants for a predicting job from a model", + "parameters": [ + { + "type": "string", + "description": "UUID of a trained model", + "name": "modelUUID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobParticipantInfoBase" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Get job's detailed info", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Delete the job. The job will be marked as delete in this site, but still viewable in other sites", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/approve": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Approve a pending job", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/data-result/download": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "the result data of a Predicting or PSI job, XXX: currently it will return an error message due to a bug in FATE", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/refresh": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Refresh the latest job status", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/job/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Job" + ], + "summary": "Disapprove a pending job", + "parameters": [ + { + "type": "string", + "description": "Job UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get model list", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ModelListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/internal/event/create": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Handle model creation event, called by the job context only", + "parameters": [ + { + "description": "Creation Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ModelCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get model's detailed info", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ModelDetail" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Delete the model", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}/publish": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Publish model to online serving system", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Creation Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ModelDeploymentRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.ModelDeployment" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/model/{uuid}/supportedDeploymentTypes": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Model" + ], + "summary": "Get list of deployment types (KFServing, FATE-Serving, etc.) this model can use", + "parameters": [ + { + "type": "string", + "description": "Model UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List all project", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ProjectList" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create a new project", + "parameters": [ + { + "description": "Basic project info", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/data/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process data sync event, to sync the data association info from fml manager", + "parameters": [ + { + "description": "Info of the project to by synced", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectResourceSyncRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/list/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process list sync event, to sync the projects list status from fml manager", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/participant/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info sync event, to sync the participant info from fml manager", + "parameters": [ + { + "description": "Info of the project to by synced", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectResourceSyncRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/event/participant/update": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant info update event, called by the FML manager only", + "parameters": [ + { + "description": "Updated participant info", + "name": "participant", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectParticipantBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project invitation, called by FML manager only", + "parameters": [ + { + "description": "invitation request", + "name": "invitation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectInvitationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/accept": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation acceptance response, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation rejection response, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/invitation/{uuid}/revoke": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process invitation revocation request, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Invitation UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process project closing event, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/data/associate": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Add associated remote data to current project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Remote data information", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ProjectData" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/data/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Dismiss associated remote data from current project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Data UUID list", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participant/{siteUUID}/dismiss": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant dismissal event, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participant/{siteUUID}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Process participant leaving request, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Site UUID", + "name": "siteUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/internal/{uuid}/participants": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create joined participants for a project, called by FML manager only", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "participants list", + "name": "participantList", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ProjectParticipant" + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get project's detailed info", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.ProjectInfo" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/autoapprovalstatus": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Change a project's auto-approval status", + "parameters": [ + { + "description": "The auto-approval status, only an 'enabled' field", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectAutoApprovalStatus" + } + }, + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/close": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Close the managed project, only available to project managing site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get associated data list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "participant uuid, i.e. the providing site uuid; if set, only returns the associated data of the specified participant", + "name": "participant", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectData" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Associate local data to current project", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Local data info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectDataAssociationRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/local": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get available local data for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectData" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/data/{dataUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Remove associated data from the current project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Data UUID", + "name": "dataUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/invitation": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Invite other site to this project", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "target site information", + "name": "info", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.ProjectParticipantBase" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/job": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get job list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobListItemBase" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Create a new job", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "description": "Job requests, only fill related field according to job type", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.JobListItemBase" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/join": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Join a pending/invited project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/leave": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Leave the joined project created by other site", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/model": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get model list for this project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ModelListItem" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "List participants", + "parameters": [ + { + "type": "string", + "description": "project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "if set to true, returns all sites, including not joined ones", + "name": "all", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectParticipant" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/participant/{participantUUID}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Remove pending participant (revoke invitation) or dismiss joined participant", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Participant UUID", + "name": "participantUUID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/project/{uuid}/reject": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Reject to join a pending/invited project", + "parameters": [ + { + "type": "string", + "description": "Project UUID", + "name": "uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Return site data", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Site" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Update site information", + "parameters": [ + { + "description": "The site information, some info like id, UUID, connected status cannot be changed", + "name": "site", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.Site" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/fateflow/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Test site connection to fate flow service", + "parameters": [ + { + "description": "The fate flow connection info", + "name": "connectInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FATEFlowConnectionInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/fmlmanager/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Connect to fml manager and register itself", + "parameters": [ + { + "description": "The FML Manager endpoint", + "name": "connectInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.FMLManagerConnectionInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/site/kubeflow/connect": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Site" + ], + "summary": "Test site connection to Kubeflow, including MinIO and KFServing", + "parameters": [ + { + "description": "The Kubeflow config info", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.KubeflowConfig" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List all saved users", + "responses": { + "200": { + "description": "Success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.PublicUser" + } + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/current": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Return current user in the jwt token", + "responses": { + "200": { + "description": "Success, the name of current user", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "login to site portal", + "parameters": [ + { + "description": "credentials for login", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + } + } + } + }, + "/user/logout": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "logout from the site portal", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{id}/password": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user Password", + "parameters": [ + { + "description": "current and new password", + "name": "passwordChangeInfo", + "in": "body", + "schema": { + "$ref": "#/definitions/service.PwdChangeInfo" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + }, + "/user/{id}/permission": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Update user permission", + "parameters": [ + { + "description": "Permission, must contain all permissions, otherwise the missing once will be considered as false", + "name": "permission", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/valueobject.UserPermissionInfo" + } + }, + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "401": { + "description": "Unauthorized operation", + "schema": { + "$ref": "#/definitions/api.GeneralResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/api.GeneralResponse" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "api.GeneralResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "example": 0 + }, + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "success" + } + } + }, + "entity.ModelDeployment": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "model_uuid": { + "type": "string" + }, + "parameters_json": { + "type": "string" + }, + "request_json": { + "type": "string" + }, + "result_json": { + "type": "string" + }, + "service_name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ProjectData": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "data_uuid": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "table_name": { + "type": "string" + }, + "table_namespace": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "update_time": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.ProjectParticipant": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + }, + "site_description": { + "type": "string" + }, + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "entity.Site": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "description": { + "description": "Description contains more text about this site", + "type": "string" + }, + "external_host": { + "description": "ExternalHost is the IP or hostname this site portal service is exposed", + "type": "string" + }, + "external_port": { + "description": "ExternalPort the port number this site portal service is exposed", + "type": "integer" + }, + "fate_flow_connected": { + "description": "FATEFlowConnected is whether the portal has connected to FATEFlow after the address is configured", + "type": "boolean" + }, + "fate_flow_connected_at": { + "description": "FATEFlowConnectedAt is the last time this portal connected to the FATE flow", + "type": "string" + }, + "fate_flow_grpc_port": { + "description": "FATEFlowGRPCPort is the grpc port number of the FATE-flow service, currently not used", + "type": "integer" + }, + "fate_flow_host": { + "description": "FATEFlowHost is the host address of the FATE-flow service", + "type": "string" + }, + "fate_flow_http_port": { + "description": "FATEFlowHTTPPort is the http port number of the FATE-flow service", + "type": "integer" + }, + "fml_manager_connected": { + "description": "FMLManagerConnected is whether the portal is connected to FML manager", + "type": "boolean" + }, + "fml_manager_connected_at": { + "description": "FMLManagerConnectedAt is the last time this portal has registered to a FML manager", + "type": "string" + }, + "fml_manager_endpoint": { + "description": "FMLManagerEndpoint is of format \"\u003chttp or https\u003e://\u003chost\u003e:\u003cport\u003e\"", + "type": "string" + }, + "fml_manager_server_name": { + "description": "FMLManagerServerName is used to verify FML Manager's certificate", + "type": "string" + }, + "https": { + "description": "HTTPS choose if site portal has HTTPS enabled", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "kubeflow_config": { + "description": "KubeflowConfig records the Kubeflow related information for deploying horizontal model to the KFServing system", + "$ref": "#/definitions/valueobject.KubeflowConfig" + }, + "kubeflow_connected": { + "description": "KubeflowConnected is whether this site has connected to the Kubeflow since it is configured", + "type": "boolean" + }, + "kubeflow_connected_at": { + "description": "KubeflowConnectedAt is the last time this portal has successfully connected all related Kubeflow service", + "type": "string" + }, + "name": { + "description": "Name is the site's name", + "type": "string" + }, + "party_id": { + "description": "PartyID is the id of this party", + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "service.FATEFlowConnectionInfo": { + "type": "object", + "properties": { + "host": { + "description": "Host address", + "type": "string" + }, + "https": { + "description": "Https is whether https is enabled", + "type": "boolean" + }, + "port": { + "description": "Port is the port number", + "type": "integer" + } + } + }, + "service.FMLManagerConnectionInfo": { + "type": "object", + "properties": { + "endpoint": { + "description": "Endpoint address starting with \"http\" or \"https\"", + "type": "string" + }, + "server_name": { + "description": "ServerName is used by Site Portal to verify FML Manager's certificate", + "type": "string" + } + } + }, + "service.GenerateJobConfRequest": { + "type": "object", + "properties": { + "dag_json": { + "$ref": "#/definitions/service.JobRawDagJson" + }, + "job_conf": { + "$ref": "#/definitions/service.JobSubmissionRequest" + } + } + }, + "service.IntersectionJobResultInfo": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "header": { + "type": "array", + "items": { + "type": "string" + } + }, + "intersect_number": { + "type": "integer" + }, + "intersect_rate": { + "type": "number" + } + } + }, + "service.JobApprovalContext": { + "type": "object", + "properties": { + "approved": { + "type": "boolean" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobConf": { + "type": "object", + "properties": { + "conf_json": { + "type": "string" + }, + "dsl_json": { + "type": "string" + } + } + }, + "service.JobData": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_local": { + "type": "boolean" + }, + "label_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providing_site_name": { + "type": "string" + }, + "providing_site_party_id": { + "type": "integer" + }, + "providing_site_uuid": { + "type": "string" + }, + "site_status": { + "type": "integer" + }, + "site_status_str": { + "type": "string" + } + } + }, + "service.JobDataBase": { + "type": "object", + "properties": { + "data_uuid": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, + "service.JobDetail": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_name": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "initiating_site_name": { + "type": "string" + }, + "initiating_site_party_id": { + "type": "integer" + }, + "initiating_site_uuid": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobData" + }, + "is_initiator": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobData" + } + }, + "pending_on_this_site": { + "type": "boolean" + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "result_info": { + "$ref": "#/definitions/service.JobResultInfo" + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + }, + "status_str": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobListItemBase": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_name": { + "type": "string" + }, + "finish_time": { + "type": "string" + }, + "initiating_site_name": { + "type": "string" + }, + "initiating_site_party_id": { + "type": "integer" + }, + "initiating_site_uuid": { + "type": "string" + }, + "is_initiator": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pending_on_this_site": { + "type": "boolean" + }, + "project_uuid": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "status_str": { + "type": "string" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.JobParticipantInfoBase": { + "type": "object", + "properties": { + "site_name": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + } + } + }, + "service.JobRawDagJson": { + "type": "object", + "properties": { + "raw_json": { + "type": "string" + } + } + }, + "service.JobResultInfo": { + "type": "object", + "properties": { + "intersection_result": { + "$ref": "#/definitions/service.IntersectionJobResultInfo" + }, + "predicting_result": { + "$ref": "#/definitions/service.PredictingJobResultInfo" + }, + "training_result": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "service.JobStatusUpdateContext": { + "type": "object", + "properties": { + "fate_job_id": { + "type": "string" + }, + "fate_job_status": { + "type": "string" + }, + "fate_model_id": { + "type": "string" + }, + "fate_model_version": { + "type": "string" + }, + "participant_status_map": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "status": { + "type": "integer" + }, + "status_message": { + "type": "string" + } + } + }, + "service.JobSubmissionRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.LocalDataDetail": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "feature_size": { + "type": "integer" + }, + "features_array": { + "type": "array", + "items": { + "type": "string" + } + }, + "filename": { + "type": "string" + }, + "id_meta_info": { + "$ref": "#/definitions/valueobject.IDMetaInfo" + }, + "name": { + "type": "string" + }, + "preview_array": { + "type": "string" + }, + "sample_size": { + "type": "integer" + }, + "table_name": { + "type": "string" + }, + "upload_job_status": { + "type": "integer" + } + } + }, + "service.LocalDataIDMetaInfoUpdateRequest": { + "type": "object", + "properties": { + "id_encryption_type": { + "type": "integer" + }, + "id_type": { + "type": "integer" + } + } + }, + "service.LocalDataListItem": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "feature_size": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sample_size": { + "type": "integer" + }, + "upload_job_status": { + "type": "integer" + } + } + }, + "service.LoginInfo": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.ModelCreationRequest": { + "type": "object", + "properties": { + "algorithm_type": { + "type": "integer" + }, + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "evaluation": { + "$ref": "#/definitions/valueobject.ModelEvaluation" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ModelDeploymentRequest": { + "type": "object", + "properties": { + "deployment_type": { + "type": "integer" + }, + "parameters_json": { + "type": "string" + }, + "service_name": { + "type": "string" + } + } + }, + "service.ModelDetail": { + "type": "object", + "properties": { + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "evaluation": { + "$ref": "#/definitions/valueobject.ModelEvaluation" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ModelListItem": { + "type": "object", + "properties": { + "component_name": { + "type": "string" + }, + "create_time": { + "type": "string" + }, + "job_name": { + "type": "string" + }, + "job_uuid": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "model_version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "role": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PredictingJobResultInfo": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "header": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "service.ProjectAutoApprovalStatus": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "service.ProjectCreationRequest": { + "type": "object", + "properties": { + "auto_approval_enabled": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.ProjectData": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "data_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_local": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "providing_site_name": { + "type": "string" + }, + "providing_site_party_id": { + "type": "integer" + }, + "providing_site_uuid": { + "type": "string" + }, + "update_time": { + "type": "string" + } + } + }, + "service.ProjectDataAssociationRequest": { + "type": "object", + "properties": { + "data_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "service.ProjectInfo": { + "type": "object", + "properties": { + "auto_approval_enabled": { + "type": "boolean" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectInvitationRequest": { + "type": "object", + "properties": { + "project_auto_approval_enabled": { + "type": "boolean" + }, + "project_creation_time": { + "type": "string" + }, + "project_description": { + "type": "string" + }, + "project_manager": { + "type": "string" + }, + "project_managing_site_name": { + "type": "string" + }, + "project_managing_site_party_id": { + "type": "integer" + }, + "project_managing_site_uuid": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "site_party_id": { + "type": "integer" + }, + "site_uuid": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectList": { + "type": "object", + "properties": { + "closed_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItemClosed" + } + }, + "invited_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItemBase" + } + }, + "joined_projects": { + "type": "array", + "items": { + "$ref": "#/definitions/service.ProjectListItem" + } + } + } + }, + "service.ProjectListItem": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "local_data_num": { + "type": "integer" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "participants_num": { + "type": "integer" + }, + "pending_job_exist": { + "type": "boolean" + }, + "remote_data_num": { + "type": "integer" + }, + "running_job_num": { + "type": "integer" + }, + "success_job_num": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectListItemBase": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectListItemClosed": { + "type": "object", + "properties": { + "closing_status": { + "type": "string" + }, + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "managed_by_this_site": { + "type": "boolean" + }, + "manager": { + "type": "string" + }, + "managing_site_name": { + "type": "string" + }, + "managing_site_party_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectParticipant": { + "type": "object", + "properties": { + "creation_time": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_current_site": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectParticipantBase": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "party_id": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + } + }, + "service.ProjectResourceSyncRequest": { + "type": "object", + "properties": { + "project_uuid": { + "type": "string" + } + } + }, + "service.PublicUser": { + "type": "object", + "properties": { + "fateboard_access": { + "description": "FATEBoardAccess controls whether the user can access fate board", + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "notebook_access": { + "description": "NoteBookAccess controls whether the user can access notebook", + "type": "boolean" + }, + "site_portal_access": { + "description": "SitePortalAccess controls whether the user can access site portal", + "type": "boolean" + }, + "uuid": { + "type": "string" + } + } + }, + "service.PwdChangeInfo": { + "type": "object", + "properties": { + "cur_password": { + "type": "string" + }, + "new_password": { + "type": "string" + } + } + }, + "service.RemoteJobCreationRequest": { + "type": "object", + "properties": { + "algorithm_component_name": { + "type": "string" + }, + "conf_json": { + "type": "string" + }, + "description": { + "type": "string" + }, + "dsl_json": { + "type": "string" + }, + "evaluate_component_name": { + "type": "string" + }, + "initiator_data": { + "$ref": "#/definitions/service.JobDataBase" + }, + "name": { + "type": "string" + }, + "other_site_data": { + "type": "array", + "items": { + "$ref": "#/definitions/service.JobDataBase" + } + }, + "predicting_model_uuid": { + "type": "string" + }, + "project_uuid": { + "type": "string" + }, + "training_algorithm_type": { + "type": "integer" + }, + "training_component_list_to_deploy": { + "type": "array", + "items": { + "type": "string" + } + }, + "training_model_name": { + "type": "string" + }, + "training_validation_enabled": { + "type": "boolean" + }, + "training_validation_percent": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "valueobject.IDMetaInfo": { + "type": "object", + "properties": { + "id_encryption_type": { + "type": "integer" + }, + "id_type": { + "type": "integer" + } + } + }, + "valueobject.KubeflowConfig": { + "type": "object", + "properties": { + "kubeconfig": { + "description": "KubeConfig is the content of the kubeconfig file to connect to kubernetes", + "type": "string" + }, + "minio_access_key": { + "description": "MinIOAccessKey is the access-key for the MinIO server", + "type": "string" + }, + "minio_endpoint": { + "description": "MinIOEndpoint is the address for the MinIO server", + "type": "string" + }, + "minio_region": { + "description": "MinIORegion is the region of the MinIO service", + "type": "string" + }, + "minio_secret_key": { + "description": "MinIOSecretKey is the secret-key for the MinIO server", + "type": "string" + }, + "minio_ssl_enabled": { + "description": "MinIOSSLEnabled is whether this connection should be over ssl", + "type": "boolean" + } + } + }, + "valueobject.ModelEvaluation": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "valueobject.UserPermissionInfo": { + "type": "object", + "properties": { + "fateboard_access": { + "description": "FATEBoardAccess controls whether the user can access fate board", + "type": "boolean" + }, + "notebook_access": { + "description": "NoteBookAccess controls whether the user can access notebook", + "type": "boolean" + }, + "site_portal_access": { + "description": "SitePortalAccess controls whether the user can access site portal", + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/site-portal/server/docs/swagger.yaml b/site-portal/server/docs/swagger.yaml new file mode 100644 index 00000000..6498d47a --- /dev/null +++ b/site-portal/server/docs/swagger.yaml @@ -0,0 +1,3343 @@ +basePath: /api/v1 +definitions: + api.GeneralResponse: + properties: + code: + example: 0 + type: integer + data: + type: object + message: + example: success + type: string + type: object + entity.ModelDeployment: + properties: + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + model_uuid: + type: string + parameters_json: + type: string + request_json: + type: string + result_json: + type: string + service_name: + type: string + status: + type: integer + type: + type: integer + updatedAt: + type: string + uuid: + type: string + type: object + entity.ProjectData: + properties: + createdAt: + type: string + creation_time: + type: string + data_uuid: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + description: + type: string + id: + type: integer + name: + type: string + project_uuid: + type: string + site_name: + type: string + site_party_id: + type: integer + site_uuid: + type: string + status: + type: integer + table_name: + type: string + table_namespace: + type: string + type: + type: integer + update_time: + type: string + updatedAt: + type: string + uuid: + type: string + type: object + entity.ProjectParticipant: + properties: + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + project_uuid: + type: string + site_description: + type: string + site_name: + type: string + site_party_id: + type: integer + site_uuid: + type: string + status: + type: integer + updatedAt: + type: string + uuid: + type: string + type: object + entity.Site: + properties: + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + description: + description: Description contains more text about this site + type: string + external_host: + description: ExternalHost is the IP or hostname this site portal service is + exposed + type: string + external_port: + description: ExternalPort the port number this site portal service is exposed + type: integer + fate_flow_connected: + description: FATEFlowConnected is whether the portal has connected to FATEFlow + after the address is configured + type: boolean + fate_flow_connected_at: + description: FATEFlowConnectedAt is the last time this portal connected to + the FATE flow + type: string + fate_flow_grpc_port: + description: FATEFlowGRPCPort is the grpc port number of the FATE-flow service, + currently not used + type: integer + fate_flow_host: + description: FATEFlowHost is the host address of the FATE-flow service + type: string + fate_flow_http_port: + description: FATEFlowHTTPPort is the http port number of the FATE-flow service + type: integer + fml_manager_connected: + description: FMLManagerConnected is whether the portal is connected to FML + manager + type: boolean + fml_manager_connected_at: + description: FMLManagerConnectedAt is the last time this portal has registered + to a FML manager + type: string + fml_manager_endpoint: + description: FMLManagerEndpoint is of format "://:" + type: string + fml_manager_server_name: + description: FMLManagerServerName is used to verify FML Manager's certificate + type: string + https: + description: HTTPS choose if site portal has HTTPS enabled + type: boolean + id: + type: integer + kubeflow_config: + $ref: '#/definitions/valueobject.KubeflowConfig' + description: KubeflowConfig records the Kubeflow related information for deploying + horizontal model to the KFServing system + kubeflow_connected: + description: KubeflowConnected is whether this site has connected to the Kubeflow + since it is configured + type: boolean + kubeflow_connected_at: + description: KubeflowConnectedAt is the last time this portal has successfully + connected all related Kubeflow service + type: string + name: + description: Name is the site's name + type: string + party_id: + description: PartyID is the id of this party + type: integer + updatedAt: + type: string + uuid: + type: string + type: object + gorm.DeletedAt: + properties: + time: + type: string + valid: + description: Valid is true if Time is not NULL + type: boolean + type: object + service.FATEFlowConnectionInfo: + properties: + host: + description: Host address + type: string + https: + description: Https is whether https is enabled + type: boolean + port: + description: Port is the port number + type: integer + type: object + service.FMLManagerConnectionInfo: + properties: + endpoint: + description: Endpoint address starting with "http" or "https" + type: string + server_name: + description: ServerName is used by Site Portal to verify FML Manager's certificate + type: string + type: object + service.GenerateJobConfRequest: + properties: + dag_json: + $ref: '#/definitions/service.JobRawDagJson' + job_conf: + $ref: '#/definitions/service.JobSubmissionRequest' + type: object + service.IntersectionJobResultInfo: + properties: + data: + items: + items: {} + type: array + type: array + header: + items: + type: string + type: array + intersect_number: + type: integer + intersect_rate: + type: number + type: object + service.JobApprovalContext: + properties: + approved: + type: boolean + site_uuid: + type: string + type: object + service.JobConf: + properties: + conf_json: + type: string + dsl_json: + type: string + type: object + service.JobData: + properties: + data_uuid: + type: string + description: + type: string + is_local: + type: boolean + label_name: + type: string + name: + type: string + providing_site_name: + type: string + providing_site_party_id: + type: integer + providing_site_uuid: + type: string + site_status: + type: integer + site_status_str: + type: string + type: object + service.JobDataBase: + properties: + data_uuid: + type: string + label_name: + type: string + type: object + service.JobDetail: + properties: + algorithm_component_name: + type: string + conf_json: + type: string + creation_time: + type: string + description: + type: string + dsl_json: + type: string + evaluate_component_name: + type: string + fate_job_id: + type: string + fate_job_status: + type: string + fate_model_name: + type: string + finish_time: + type: string + initiating_site_name: + type: string + initiating_site_party_id: + type: integer + initiating_site_uuid: + type: string + initiator_data: + $ref: '#/definitions/service.JobData' + is_initiator: + type: boolean + name: + type: string + other_site_data: + items: + $ref: '#/definitions/service.JobData' + type: array + pending_on_this_site: + type: boolean + predicting_model_uuid: + type: string + project_uuid: + type: string + result_info: + $ref: '#/definitions/service.JobResultInfo' + status: + type: integer + status_message: + type: string + status_str: + type: string + training_algorithm_type: + type: integer + training_component_list_to_deploy: + items: + type: string + type: array + training_model_name: + type: string + training_validation_enabled: + type: boolean + training_validation_percent: + type: integer + type: + type: integer + username: + type: string + uuid: + type: string + type: object + service.JobListItemBase: + properties: + creation_time: + type: string + description: + type: string + fate_job_id: + type: string + fate_job_status: + type: string + fate_model_name: + type: string + finish_time: + type: string + initiating_site_name: + type: string + initiating_site_party_id: + type: integer + initiating_site_uuid: + type: string + is_initiator: + type: boolean + name: + type: string + pending_on_this_site: + type: boolean + project_uuid: + type: string + status: + type: integer + status_str: + type: string + type: + type: integer + username: + type: string + uuid: + type: string + type: object + service.JobParticipantInfoBase: + properties: + site_name: + type: string + site_party_id: + type: integer + site_uuid: + type: string + type: object + service.JobRawDagJson: + properties: + raw_json: + type: string + type: object + service.JobResultInfo: + properties: + intersection_result: + $ref: '#/definitions/service.IntersectionJobResultInfo' + predicting_result: + $ref: '#/definitions/service.PredictingJobResultInfo' + training_result: + additionalProperties: + type: string + type: object + type: object + service.JobStatusUpdateContext: + properties: + fate_job_id: + type: string + fate_job_status: + type: string + fate_model_id: + type: string + fate_model_version: + type: string + participant_status_map: + additionalProperties: + type: integer + type: object + status: + type: integer + status_message: + type: string + type: object + service.JobSubmissionRequest: + properties: + algorithm_component_name: + type: string + conf_json: + type: string + description: + type: string + dsl_json: + type: string + evaluate_component_name: + type: string + initiator_data: + $ref: '#/definitions/service.JobDataBase' + name: + type: string + other_site_data: + items: + $ref: '#/definitions/service.JobDataBase' + type: array + predicting_model_uuid: + type: string + project_uuid: + type: string + training_algorithm_type: + type: integer + training_component_list_to_deploy: + items: + type: string + type: array + training_model_name: + type: string + training_validation_enabled: + type: boolean + training_validation_percent: + type: integer + type: + type: integer + type: object + service.LocalDataDetail: + properties: + creation_time: + type: string + data_id: + type: string + description: + type: string + feature_size: + type: integer + features_array: + items: + type: string + type: array + filename: + type: string + id_meta_info: + $ref: '#/definitions/valueobject.IDMetaInfo' + name: + type: string + preview_array: + type: string + sample_size: + type: integer + table_name: + type: string + upload_job_status: + type: integer + type: object + service.LocalDataIDMetaInfoUpdateRequest: + properties: + id_encryption_type: + type: integer + id_type: + type: integer + type: object + service.LocalDataListItem: + properties: + creation_time: + type: string + data_id: + type: string + feature_size: + type: integer + name: + type: string + sample_size: + type: integer + upload_job_status: + type: integer + type: object + service.LoginInfo: + properties: + password: + type: string + username: + type: string + type: object + service.ModelCreationRequest: + properties: + algorithm_type: + type: integer + component_name: + type: string + create_time: + type: string + evaluation: + $ref: '#/definitions/valueobject.ModelEvaluation' + job_name: + type: string + job_uuid: + type: string + model_id: + type: string + model_version: + type: string + name: + type: string + party_id: + type: integer + project_uuid: + type: string + role: + type: string + uuid: + type: string + type: object + service.ModelDeploymentRequest: + properties: + deployment_type: + type: integer + parameters_json: + type: string + service_name: + type: string + type: object + service.ModelDetail: + properties: + component_name: + type: string + create_time: + type: string + evaluation: + $ref: '#/definitions/valueobject.ModelEvaluation' + job_name: + type: string + job_uuid: + type: string + model_id: + type: string + model_version: + type: string + name: + type: string + party_id: + type: integer + project_name: + type: string + project_uuid: + type: string + role: + type: string + uuid: + type: string + type: object + service.ModelListItem: + properties: + component_name: + type: string + create_time: + type: string + job_name: + type: string + job_uuid: + type: string + model_id: + type: string + model_version: + type: string + name: + type: string + party_id: + type: integer + project_name: + type: string + project_uuid: + type: string + role: + type: string + uuid: + type: string + type: object + service.PredictingJobResultInfo: + properties: + count: + type: integer + data: + items: + items: {} + type: array + type: array + header: + items: + type: string + type: array + type: object + service.ProjectAutoApprovalStatus: + properties: + enabled: + type: boolean + type: object + service.ProjectCreationRequest: + properties: + auto_approval_enabled: + type: boolean + description: + type: string + name: + type: string + type: object + service.ProjectData: + properties: + creation_time: + type: string + data_id: + type: string + description: + type: string + is_local: + type: boolean + name: + type: string + providing_site_name: + type: string + providing_site_party_id: + type: integer + providing_site_uuid: + type: string + update_time: + type: string + type: object + service.ProjectDataAssociationRequest: + properties: + data_id: + type: string + name: + type: string + type: object + service.ProjectInfo: + properties: + auto_approval_enabled: + type: boolean + creation_time: + type: string + description: + type: string + managed_by_this_site: + type: boolean + manager: + type: string + managing_site_name: + type: string + managing_site_party_id: + type: integer + name: + type: string + uuid: + type: string + type: object + service.ProjectInvitationRequest: + properties: + project_auto_approval_enabled: + type: boolean + project_creation_time: + type: string + project_description: + type: string + project_manager: + type: string + project_managing_site_name: + type: string + project_managing_site_party_id: + type: integer + project_managing_site_uuid: + type: string + project_name: + type: string + project_uuid: + type: string + site_party_id: + type: integer + site_uuid: + type: string + uuid: + type: string + type: object + service.ProjectList: + properties: + closed_projects: + items: + $ref: '#/definitions/service.ProjectListItemClosed' + type: array + invited_projects: + items: + $ref: '#/definitions/service.ProjectListItemBase' + type: array + joined_projects: + items: + $ref: '#/definitions/service.ProjectListItem' + type: array + type: object + service.ProjectListItem: + properties: + creation_time: + type: string + description: + type: string + local_data_num: + type: integer + managed_by_this_site: + type: boolean + manager: + type: string + managing_site_name: + type: string + managing_site_party_id: + type: integer + name: + type: string + participants_num: + type: integer + pending_job_exist: + type: boolean + remote_data_num: + type: integer + running_job_num: + type: integer + success_job_num: + type: integer + uuid: + type: string + type: object + service.ProjectListItemBase: + properties: + creation_time: + type: string + description: + type: string + managed_by_this_site: + type: boolean + manager: + type: string + managing_site_name: + type: string + managing_site_party_id: + type: integer + name: + type: string + uuid: + type: string + type: object + service.ProjectListItemClosed: + properties: + closing_status: + type: string + creation_time: + type: string + description: + type: string + managed_by_this_site: + type: boolean + manager: + type: string + managing_site_name: + type: string + managing_site_party_id: + type: integer + name: + type: string + uuid: + type: string + type: object + service.ProjectParticipant: + properties: + creation_time: + type: string + description: + type: string + is_current_site: + type: boolean + name: + type: string + party_id: + type: integer + status: + type: integer + uuid: + type: string + type: object + service.ProjectParticipantBase: + properties: + description: + type: string + name: + type: string + party_id: + type: integer + uuid: + type: string + type: object + service.ProjectResourceSyncRequest: + properties: + project_uuid: + type: string + type: object + service.PublicUser: + properties: + fateboard_access: + description: FATEBoardAccess controls whether the user can access fate board + type: boolean + id: + type: integer + name: + type: string + notebook_access: + description: NoteBookAccess controls whether the user can access notebook + type: boolean + site_portal_access: + description: SitePortalAccess controls whether the user can access site portal + type: boolean + uuid: + type: string + type: object + service.PwdChangeInfo: + properties: + cur_password: + type: string + new_password: + type: string + type: object + service.RemoteJobCreationRequest: + properties: + algorithm_component_name: + type: string + conf_json: + type: string + description: + type: string + dsl_json: + type: string + evaluate_component_name: + type: string + initiator_data: + $ref: '#/definitions/service.JobDataBase' + name: + type: string + other_site_data: + items: + $ref: '#/definitions/service.JobDataBase' + type: array + predicting_model_uuid: + type: string + project_uuid: + type: string + training_algorithm_type: + type: integer + training_component_list_to_deploy: + items: + type: string + type: array + training_model_name: + type: string + training_validation_enabled: + type: boolean + training_validation_percent: + type: integer + type: + type: integer + username: + type: string + uuid: + type: string + type: object + valueobject.IDMetaInfo: + properties: + id_encryption_type: + type: integer + id_type: + type: integer + type: object + valueobject.KubeflowConfig: + properties: + kubeconfig: + description: KubeConfig is the content of the kubeconfig file to connect to + kubernetes + type: string + minio_access_key: + description: MinIOAccessKey is the access-key for the MinIO server + type: string + minio_endpoint: + description: MinIOEndpoint is the address for the MinIO server + type: string + minio_region: + description: MinIORegion is the region of the MinIO service + type: string + minio_secret_key: + description: MinIOSecretKey is the secret-key for the MinIO server + type: string + minio_ssl_enabled: + description: MinIOSSLEnabled is whether this connection should be over ssl + type: boolean + type: object + valueobject.ModelEvaluation: + additionalProperties: + type: string + type: object + valueobject.UserPermissionInfo: + properties: + fateboard_access: + description: FATEBoardAccess controls whether the user can access fate board + type: boolean + notebook_access: + description: NoteBookAccess controls whether the user can access notebook + type: boolean + site_portal_access: + description: SitePortalAccess controls whether the user can access site portal + type: boolean + type: object +info: + contact: + name: FedLCM team + description: backend APIs of site portal service + termsOfService: http://swagger.io/terms/ + title: site portal API service + version: v1 +paths: + /data: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.LocalDataListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all data records + tags: + - LocalData + post: + parameters: + - description: The csv file + in: formData + name: file + required: true + type: file + - description: Data name + in: formData + name: name + required: true + type: string + - description: Data description + in: formData + name: description + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success, the data field is the data UUID + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Upload a local csv data + tags: + - LocalData + /data/{uuid}: + delete: + parameters: + - description: Data UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the data file, both the local copy and the FATE table + tags: + - LocalData + get: + parameters: + - description: Data UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.LocalDataDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get data record's detailed info + tags: + - LocalData + /data/{uuid}/columns: + get: + parameters: + - description: Data UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + type: string + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get data headers + tags: + - LocalData + /data/{uuid}/file: + get: + parameters: + - description: Data UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Download data file + tags: + - LocalData + /data/{uuid}/idmetainfo: + put: + parameters: + - description: The meta info + in: body + name: info + required: true + schema: + $ref: '#/definitions/service.LocalDataIDMetaInfoUpdateRequest' + - description: Data UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Update data record's ID meta info + tags: + - LocalData + /job/{uuid}: + delete: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the job. The job will be marked as delete in this site, but + still viewable in other sites + tags: + - Job + get: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.JobDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get job's detailed info + tags: + - Job + /job/{uuid}/approve: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Approve a pending job + tags: + - Job + /job/{uuid}/data-result/download: + get: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: 'the result data of a Predicting or PSI job, XXX: currently it will + return an error message due to a bug in FATE' + tags: + - Job + /job/{uuid}/refresh: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Refresh the latest job status + tags: + - Job + /job/{uuid}/reject: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Disapprove a pending job + tags: + - Job + /job/components: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get all the components and their default configs. The returned format + is json + tags: + - Job + /job/conf/create: + post: + parameters: + - description: 'Job requests, not all fields are required: only need to fill + related ones according to job type' + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.JobSubmissionRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.JobConf' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get a job's config template, used in json template mode + tags: + - Job + /job/generateConfFromDag: + post: + parameters: + - description: The request for generate the conf json file + in: body + name: generateJobConfRequest + required: true + schema: + $ref: '#/definitions/service.GenerateJobConfRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Generate the conf json file from the DAG the user draw, the conf file + can be consumed by Fateflow + tags: + - Job + /job/generateDslFromDag: + post: + parameters: + - description: The raw json, the value should be a serialized json string + in: body + name: rawJson + required: true + schema: + $ref: '#/definitions/service.JobRawDagJson' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Generate the DSL json file from the DAG the user draw, should be called + by UI only + tags: + - Job + /job/internal/{uuid}/response: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + - description: Approval context, containing the sender UUID and approval status + in: body + name: context + required: true + schema: + $ref: '#/definitions/service.JobApprovalContext' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Handle job response from other sites, only called by FML manager + tags: + - Job + /job/internal/{uuid}/status: + post: + parameters: + - description: Job UUID + in: path + name: uuid + required: true + type: string + - description: Job status update context, containing the latest job and participant + status + in: body + name: context + required: true + schema: + $ref: '#/definitions/service.JobStatusUpdateContext' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Handle job status updates, only called by FML manager + tags: + - Job + /job/internal/create: + post: + parameters: + - description: Job info + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.RemoteJobCreationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new job that is created by other site, only called by FML + manager + tags: + - Job + /job/predict/participant: + get: + parameters: + - description: UUID of a trained model + in: query + name: modelUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.JobParticipantInfoBase' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get allowed participants for a predicting job from a model + tags: + - Job + /model: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ModelListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get model list + tags: + - Model + /model/{uuid}: + delete: + parameters: + - description: Model UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Delete the model + tags: + - Model + get: + parameters: + - description: Model UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ModelDetail' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get model's detailed info + tags: + - Model + /model/{uuid}/publish: + post: + parameters: + - description: Model UUID + in: path + name: uuid + required: true + type: string + - description: Creation Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.ModelDeploymentRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/entity.ModelDeployment' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Publish model to online serving system + tags: + - Model + /model/{uuid}/supportedDeploymentTypes: + get: + parameters: + - description: Model UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + type: integer + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get list of deployment types (KFServing, FATE-Serving, etc.) this model + can use + tags: + - Model + /model/internal/event/create: + post: + parameters: + - description: Creation Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.ModelCreationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Handle model creation event, called by the job context only + tags: + - Model + /project: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ProjectList' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all project + tags: + - Project + post: + parameters: + - description: Basic project info + in: body + name: project + required: true + schema: + $ref: '#/definitions/service.ProjectCreationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new project + tags: + - Project + /project/{uuid}: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.ProjectInfo' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get project's detailed info + tags: + - Project + /project/{uuid}/autoapprovalstatus: + put: + parameters: + - description: The auto-approval status, only an 'enabled' field + in: body + name: status + required: true + schema: + $ref: '#/definitions/service.ProjectAutoApprovalStatus' + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Change a project's auto-approval status + tags: + - Project + /project/{uuid}/close: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Close the managed project, only available to project managing site + tags: + - Project + /project/{uuid}/data: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: participant uuid, i.e. the providing site uuid; if set, only + returns the associated data of the specified participant + in: query + name: participant + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ProjectData' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get associated data list for this project + tags: + - Project + post: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: Local data info + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.ProjectDataAssociationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Associate local data to current project + tags: + - Project + /project/{uuid}/data/{dataUUID}: + delete: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Data UUID + in: path + name: dataUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Remove associated data from the current project + tags: + - Project + /project/{uuid}/data/local: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ProjectData' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get available local data for this project + tags: + - Project + /project/{uuid}/invitation: + post: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: target site information + in: body + name: info + required: true + schema: + $ref: '#/definitions/service.ProjectParticipantBase' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Invite other site to this project + tags: + - Project + /project/{uuid}/job: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.JobListItemBase' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get job list for this project + tags: + - Project + post: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: Job requests, only fill related field according to job type + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.JobSubmissionRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/service.JobListItemBase' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create a new job + tags: + - Project + /project/{uuid}/join: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Join a pending/invited project + tags: + - Project + /project/{uuid}/leave: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Leave the joined project created by other site + tags: + - Project + /project/{uuid}/model: + get: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ModelListItem' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Get model list for this project + tags: + - Project + /project/{uuid}/participant: + get: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: if set to true, returns all sites, including not joined ones + in: query + name: all + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.ProjectParticipant' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List participants + tags: + - Project + /project/{uuid}/participant/{participantUUID}: + delete: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Participant UUID + in: path + name: participantUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Remove pending participant (revoke invitation) or dismiss joined participant + tags: + - Project + /project/{uuid}/reject: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Reject to join a pending/invited project + tags: + - Project + /project/internal/{uuid}/close: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process project closing event, called by FML manager only + tags: + - Project + /project/internal/{uuid}/data/associate: + post: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: Remote data information + in: body + name: data + required: true + schema: + items: + $ref: '#/definitions/entity.ProjectData' + type: array + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Add associated remote data to current project, called by FML manager + only + tags: + - Project + /project/internal/{uuid}/data/dismiss: + post: + parameters: + - description: project UUID + in: path + name: uuid + required: true + type: string + - description: Data UUID list + in: body + name: data + required: true + schema: + items: + type: string + type: array + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Dismiss associated remote data from current project, called by FML + manager only + tags: + - Project + /project/internal/{uuid}/participant/{siteUUID}/dismiss: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Site UUID + in: path + name: siteUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant dismissal event, called by FML manager only + tags: + - Project + /project/internal/{uuid}/participant/{siteUUID}/leave: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: Site UUID + in: path + name: siteUUID + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant leaving request, called by FML manager only + tags: + - Project + /project/internal/{uuid}/participants: + post: + parameters: + - description: Project UUID + in: path + name: uuid + required: true + type: string + - description: participants list + in: body + name: participantList + required: true + schema: + items: + $ref: '#/definitions/entity.ProjectParticipant' + type: array + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Create joined participants for a project, called by FML manager only + tags: + - Project + /project/internal/event/data/sync: + post: + parameters: + - description: Info of the project to by synced + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.ProjectResourceSyncRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process data sync event, to sync the data association info from fml + manager + tags: + - Project + /project/internal/event/list/sync: + post: + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process list sync event, to sync the projects list status from fml + manager + tags: + - Project + /project/internal/event/participant/sync: + post: + parameters: + - description: Info of the project to by synced + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.ProjectResourceSyncRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant info sync event, to sync the participant info from + fml manager + tags: + - Project + /project/internal/event/participant/update: + post: + parameters: + - description: Updated participant info + in: body + name: participant + required: true + schema: + $ref: '#/definitions/service.ProjectParticipantBase' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process participant info update event, called by the FML manager only + tags: + - Project + /project/internal/invitation: + post: + parameters: + - description: invitation request + in: body + name: invitation + required: true + schema: + $ref: '#/definitions/service.ProjectInvitationRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process project invitation, called by FML manager only + tags: + - Project + /project/internal/invitation/{uuid}/accept: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation acceptance response, called by FML manager only + tags: + - Project + /project/internal/invitation/{uuid}/reject: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation rejection response, called by FML manager only + tags: + - Project + /project/internal/invitation/{uuid}/revoke: + post: + parameters: + - description: Invitation UUID + in: path + name: uuid + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Process invitation revocation request, called by FML manager only + tags: + - Project + /site: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + $ref: '#/definitions/entity.Site' + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return site data + tags: + - Site + put: + parameters: + - description: The site information, some info like id, UUID, connected status + cannot be changed + in: body + name: site + required: true + schema: + $ref: '#/definitions/entity.Site' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Update site information + tags: + - Site + /site/fateflow/connect: + post: + parameters: + - description: The fate flow connection info + in: body + name: connectInfo + required: true + schema: + $ref: '#/definitions/service.FATEFlowConnectionInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Test site connection to fate flow service + tags: + - Site + /site/fmlmanager/connect: + post: + parameters: + - description: The FML Manager endpoint + in: body + name: connectInfo + required: true + schema: + $ref: '#/definitions/service.FMLManagerConnectionInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Connect to fml manager and register itself + tags: + - Site + /site/kubeflow/connect: + post: + parameters: + - description: The Kubeflow config info + in: body + name: config + required: true + schema: + $ref: '#/definitions/valueobject.KubeflowConfig' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Test site connection to Kubeflow, including MinIO and KFServing + tags: + - Site + /user: + get: + produces: + - application/json + responses: + "200": + description: Success + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + items: + $ref: '#/definitions/service.PublicUser' + type: array + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: List all saved users + tags: + - User + /user/{id}/password: + put: + parameters: + - description: current and new password + in: body + name: passwordChangeInfo + schema: + $ref: '#/definitions/service.PwdChangeInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Update user Password + tags: + - User + /user/{id}/permission: + put: + parameters: + - description: Permission, must contain all permissions, otherwise the missing + once will be considered as false + in: body + name: permission + required: true + schema: + $ref: '#/definitions/valueobject.UserPermissionInfo' + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Update user permission + tags: + - User + /user/current: + get: + produces: + - application/json + responses: + "200": + description: Success, the name of current user + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + data: + type: string + type: object + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: Return current user in the jwt token + tags: + - User + /user/login: + post: + parameters: + - description: credentials for login + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/service.LoginInfo' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "401": + description: Unauthorized operation + schema: + $ref: '#/definitions/api.GeneralResponse' + summary: login to site portal + tags: + - User + /user/logout: + post: + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/api.GeneralResponse' + "500": + description: Internal server error + schema: + allOf: + - $ref: '#/definitions/api.GeneralResponse' + - properties: + code: + type: integer + type: object + summary: logout from the site portal + tags: + - User +swagger: "2.0" diff --git a/site-portal/server/domain/aggregate/common.go b/site-portal/server/domain/aggregate/common.go new file mode 100644 index 00000000..5f24536e --- /dev/null +++ b/site-portal/server/domain/aggregate/common.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregate + +// FMLManagerConnectionInfo contains FML connection info, loaded from the "site" context +type FMLManagerConnectionInfo struct { + Connected bool + Endpoint string + ServerName string +} diff --git a/site-portal/server/domain/aggregate/job_aggregate.go b/site-portal/server/domain/aggregate/job_aggregate.go new file mode 100644 index 00000000..4e9272f6 --- /dev/null +++ b/site-portal/server/domain/aggregate/job_aggregate.go @@ -0,0 +1,579 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregate + +import ( + "net/http" + "strconv" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient/template" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fmlmanager" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// JobAggregate holds the job info and all the joined participants +type JobAggregate struct { + Job *entity.Job + Initiator *entity.JobParticipant + Participants map[string]*entity.JobParticipant + JobRepo repo.JobRepository + ParticipantRepo repo.JobParticipantRepository + FMLManagerConnectionInfo FMLManagerConnectionInfo + JobContext JobContext +} + +// JobContext contains necessary context for working with a job +type JobContext struct { + AutoApprovalEnabled bool + CurrentSiteUUID string +} + +// GenerateReaderConfigMaps returns maps whose key is index, and values are the reader's configurations, in +// specific, the table name and namespaces for each party. +func (aggregate *JobAggregate) GenerateReaderConfigMaps(hostUuidList []string) (hostMap, + guestMap map[string]interface{}) { + hostMap = map[string]interface{}{} + guestMap = map[string]interface{}{} + guestMap["0"] = map[string]interface{}{} + guestMap["0"].(map[string]interface{})["reader_0"] = map[string]map[string]string{ + "table": { + "name": aggregate.Initiator.DataTableName, + "namespace": aggregate.Initiator.DataTableNamespace, + }, + } + // The reason why we need the hostUuidList from the JobSubmissionRequest is that list can retain the sequence while + // map cannot. Here if we only iterate the aggregate.Participants to generate the table name and namespace. The + // sequence could be wrong and can mismatch the other configs we generate from the DAG. The good news is, the DAG's + // hosts sequence matches the hostUuidList. So here we just need to iterate hostUuidList then things will be right. + for i, hostUuid := range hostUuidList { + hostMap[strconv.Itoa(i)] = map[string]interface{}{} + hostMap[strconv.Itoa(i)].(map[string]interface{})["reader_0"] = map[string]map[string]string{ + "table": { + "name": aggregate.Participants[hostUuid].DataTableName, + "namespace": aggregate.Participants[hostUuid].DataTableNamespace, + }, + } + } + return hostMap, guestMap +} + +// GenerateGeneralTrainingConf returns a string which contains the general conf information of a training job, +// including "dsl_version", "initiator", "role" and "job_parameters". +func (aggregate *JobAggregate) GenerateGeneralTrainingConf(hostUuidList []string) (string, error) { + param := template.GeneralTrainingParam{ + Guest: template.PartyDataInfo{ + PartyID: strconv.Itoa(int(aggregate.Initiator.SitePartyID)), + }, + Hosts: nil, + } + for _, hostUuid := range hostUuidList { + param.Hosts = append(param.Hosts, template.PartyDataInfo{ + PartyID: strconv.Itoa(int(aggregate.Participants[hostUuid].SitePartyID)), + }) + } + generalConfJson, err := template.BuildTrainingConfGeneralStr(param) + if err != nil { + return "", err + } else { + return generalConfJson, nil + } +} + +// GenerateConfig returns the FATE job conf and dsl +func (aggregate *JobAggregate) GenerateConfig() (string, string, error) { + switch aggregate.Job.Type { + case entity.JobTypeTraining: + return aggregate.generateTrainingConfig() + case entity.JobTypePredict: + return aggregate.generatePredictingConfig() + case entity.JobTypePSI: + return aggregate.generatePSIConfig() + } + return "", "", errors.New("unsupported job type") +} + +func (aggregate *JobAggregate) generatePredictingConfig() (string, string, error) { + switch aggregate.Job.AlgorithmType { + case entity.JobAlgorithmTypeHomoLR, entity.JobAlgorithmTypeHomoSBT: + if len(aggregate.Participants) > 0 { + return "", "", errors.New("horizontal predicting job cannot have other participants") + } + param := template.HomoPredictingParam{ + Role: string(aggregate.Initiator.SiteRole), + ModelID: aggregate.Job.FATEModelID, + ModelVersion: aggregate.Job.FATEModelVersion, + PartyDataInfo: template.PartyDataInfo{ + PartyID: strconv.Itoa(int(aggregate.Initiator.SitePartyID)), + TableName: aggregate.Initiator.DataTableName, + TableNamespace: aggregate.Initiator.DataTableNamespace, + }, + } + return template.BuildHomoPredictingConf(param) + } + return "", "", errors.Errorf("invalied algorithm type: %d", aggregate.Job.AlgorithmType) +} + +func (aggregate *JobAggregate) generatePSIConfig() (string, string, error) { + param := template.PSIParam{ + Guest: template.PartyDataInfo{ + PartyID: strconv.Itoa(int(aggregate.Initiator.SitePartyID)), + TableName: aggregate.Initiator.DataTableName, + TableNamespace: aggregate.Initiator.DataTableNamespace, + }, + Hosts: nil, + } + for _, host := range aggregate.Participants { + param.Hosts = append(param.Hosts, template.PartyDataInfo{ + PartyID: strconv.Itoa(int(host.SitePartyID)), + TableName: host.DataTableName, + TableNamespace: host.DataTableNamespace, + }) + } + return template.BuildPsiConf(param) +} + +func (aggregate *JobAggregate) generateTrainingConfig() (string, string, error) { + switch aggregate.Job.AlgorithmType { + case entity.JobAlgorithmTypeHomoLR, entity.JobAlgorithmTypeHomoSBT: + homoAlgorithmType := template.HomoAlgorithmTypeLR + if aggregate.Job.AlgorithmType == entity.JobAlgorithmTypeHomoSBT { + homoAlgorithmType = template.HomoAlgorithmTypeSBT + } + info := template.HomoTrainingParam{ + Guest: template.PartyDataInfo{ + PartyID: strconv.Itoa(int(aggregate.Initiator.SitePartyID)), + TableName: aggregate.Initiator.DataTableName, + TableNamespace: aggregate.Initiator.DataTableNamespace, + }, + Hosts: nil, + LabelName: aggregate.Initiator.DataLabelName, + ValidationEnabled: aggregate.Job.AlgorithmConfig.TrainingValidationEnabled, + ValidationPercent: aggregate.Job.AlgorithmConfig.TrainingValidationSizePercent, + Type: homoAlgorithmType, + } + for _, host := range aggregate.Participants { + info.Hosts = append(info.Hosts, template.PartyDataInfo{ + PartyID: strconv.Itoa(int(host.SitePartyID)), + TableName: host.DataTableName, + TableNamespace: host.DataTableNamespace, + }) + } + return template.BuildHomoTrainingConf(info) + } + return "", "", errors.Errorf("invalid algorithm type: %d", aggregate.Job.AlgorithmType) +} + +// GeneratePredictingJobParticipants returns a list of participant that should join new predicting job based on the job +func (aggregate *JobAggregate) GeneratePredictingJobParticipants() ([]*entity.JobParticipant, error) { + if aggregate.Job.Type != entity.JobTypeTraining { + return nil, errors.New("invalid job type") + } + switch aggregate.Job.AlgorithmType { + case entity.JobAlgorithmTypeHomoSBT, entity.JobAlgorithmTypeHomoLR: + if participant, ok := aggregate.Participants[aggregate.JobContext.CurrentSiteUUID]; ok { + return []*entity.JobParticipant{ + participant, + }, nil + } else if aggregate.Initiator.SiteUUID == aggregate.JobContext.CurrentSiteUUID { + return []*entity.JobParticipant{ + aggregate.Initiator, + }, nil + } else { + return nil, errors.New("current site cannot participate in the predicting job") + } + } + return nil, errors.New("cannot get predicting job participant list") +} + +// SubmitJob submits the job to the FATE system +func (aggregate *JobAggregate) SubmitJob() error { + if aggregate.Job == nil { + return errors.New("job not created") + } + if len(aggregate.Participants) > 0 && !aggregate.FMLManagerConnectionInfo.Connected { + return errors.New("fml manager not connected") + } + if len(aggregate.Participants) == 0 { + if aggregate.Job.Type == entity.JobTypeTraining && aggregate.Job.AlgorithmType == entity.JobAlgorithmTypeHomoLR { + return errors.New("homo LR job cannot be launched with only one party") + } + } + if aggregate.JobContext.CurrentSiteUUID != aggregate.Job.InitiatingSiteUUID { + return errors.Errorf("initiating site %s(%s) is not the current site", aggregate.Job.InitiatingSiteName, aggregate.Job.InitiatingSiteUUID) + } + + if aggregate.Job.Conf == "" || aggregate.Job.DSL == "" { + log.Warn().Msgf("job %s has no DSL or Conf content, generating now", aggregate.Job.Name) + conf, dsl, err := aggregate.GenerateConfig() + if err != nil { + return err + } + aggregate.Job.Conf = conf + aggregate.Job.DSL = dsl + } + + if err := aggregate.Job.Validate(); err != nil { + return err + } + if err := aggregate.Job.Create(); err != nil { + return err + } + aggregate.Initiator.JobUUID = aggregate.Job.UUID + if err := func() error { + if err := aggregate.Initiator.Create(); err != nil { + return err + } + if len(aggregate.Participants) == 0 { + log.Info().Msgf("launching local job %s", aggregate.Job.UUID) + if err := aggregate.Job.SubmitToFATE(aggregate.updateJobResultInfo); err != nil { + return errors.Wrap(err, "failed to submit job to FATE") + } + if aggregate.Job.Type == entity.JobTypePSI { + return errors.New("PSI job cannot be launched with only one party") + } + } else { + log.Info().Msgf("sending job to FML Manager, job: %s, uuid: %s", aggregate.Job.Name, aggregate.Job.UUID) + for _, participant := range aggregate.Participants { + participant.JobUUID = aggregate.Job.UUID + if err := participant.Create(); err != nil { + return errors.Wrapf(err, "failed to create participant: %s", participant.SiteUUID) + } + } + + client := fmlmanager.NewFMLManagerClient(aggregate.FMLManagerConnectionInfo.Endpoint, aggregate.FMLManagerConnectionInfo.ServerName) + if err := client.SendJobCreationRequest(aggregate.Job.UUID, aggregate.Job.InitiatingUser, aggregate.Job.RequestJson); err != nil { + return errors.Wrap(err, "failed to create job to fml manager") + } + } + return nil + }(); err != nil { + _ = aggregate.Job.UpdateStatus(entity.JobStatusDeleted) + return err + } + return nil +} + +// ApproveJob mark the job as approved and notify FML manager +func (aggregate *JobAggregate) ApproveJob() error { + if !aggregate.FMLManagerConnectionInfo.Connected { + return errors.New("fml manager not connected") + } + participant, ok := aggregate.Participants[aggregate.JobContext.CurrentSiteUUID] + if !ok { + return errors.Errorf("cannot find participant %s", aggregate.JobContext.CurrentSiteUUID) + } + + client := fmlmanager.NewFMLManagerClient(aggregate.FMLManagerConnectionInfo.Endpoint, aggregate.FMLManagerConnectionInfo.ServerName) + if err := client.SendJobApprovalResponse(aggregate.Job.UUID, fmlmanager.JobApprovalContext{ + SiteUUID: aggregate.JobContext.CurrentSiteUUID, + Approved: true, + }); err != nil { + return errors.Wrap(err, "failed to send job approval to fml manager") + } + if err := participant.UpdateStatus(entity.JobParticipantStatusApproved); err != nil { + return err + } + return nil +} + +// RejectJob mark the job as rejected and notify the FML manager +func (aggregate *JobAggregate) RejectJob() error { + if !aggregate.FMLManagerConnectionInfo.Connected { + return errors.New("fml manager not connected") + } + participant, ok := aggregate.Participants[aggregate.JobContext.CurrentSiteUUID] + if !ok { + return errors.Errorf("cannot find participant %s", aggregate.JobContext.CurrentSiteUUID) + } + + client := fmlmanager.NewFMLManagerClient(aggregate.FMLManagerConnectionInfo.Endpoint, aggregate.FMLManagerConnectionInfo.ServerName) + if err := client.SendJobApprovalResponse(aggregate.Job.UUID, fmlmanager.JobApprovalContext{ + SiteUUID: aggregate.JobContext.CurrentSiteUUID, + Approved: false, + }); err != nil { + return errors.Wrap(err, "failed to send job approval to fml manager") + } + + if err := participant.UpdateStatus(entity.JobParticipantStatusRejected); err != nil { + return err + } + if err := aggregate.Job.UpdateStatus(entity.JobStatusRejected); err != nil { + return err + } + return nil +} + +// HandleRemoteJobCreation handles creation of job initiated by other sites +func (aggregate *JobAggregate) HandleRemoteJobCreation() error { + if aggregate.Job.UUID == "" { + return errors.New("missing job uuid") + } + _, err := aggregate.JobRepo.GetByUUID(aggregate.Job.UUID) + if err != nil { + if errors.Is(err, repo.ErrJobNotFound) { + if err := aggregate.Job.Create(); err != nil { + return errors.Wrapf(err, "failed to create job") + } + } else { + return errors.Wrap(err, "failed to query job") + } + } else { + log.Warn().Msgf("updating existing job to pending status, job: %s, uuid: %s", aggregate.Job.Name, aggregate.Job.UUID) + if err := aggregate.Job.UpdateStatus(entity.JobStatusPending); err != nil { + return errors.Wrapf(err, "failed to update job info") + } + } + + aggregate.Initiator.JobUUID = aggregate.Job.UUID + if err := aggregate.createOrUpdateParticipant(aggregate.Initiator); err != nil { + return err + } + + for _, participant := range aggregate.Participants { + participant.JobUUID = aggregate.Job.UUID + if err := aggregate.createOrUpdateParticipant(participant); err != nil { + return err + } + } + + if aggregate.JobContext.AutoApprovalEnabled { + go func() { + // add a delay to make sure other sites are in-sync + time.Sleep(time.Second * 10) + log.Info().Msgf("auto approving job %s(%s)", aggregate.Job.Name, aggregate.Job.UUID) + if err := aggregate.ApproveJob(); err != nil { + log.Err(err).Msgf("failed to approve job: %s", aggregate.Job.UUID) + } + }() + } + return nil +} + +// handleJobApproval process job approval, and may start the job if all participants approved it +func (aggregate *JobAggregate) handleJobApproval(siteUUID string) error { + participant, ok := aggregate.Participants[siteUUID] + if !ok { + return errors.Errorf("cannot find participant %s", siteUUID) + } + + if err := participant.UpdateStatus(entity.JobParticipantStatusApproved); err != nil { + return err + } + + go func() { + // reload participants as others may have updated them + if err := aggregate.reloadParticipant(); err != nil { + log.Err(err).Msg("failed to reload participant") + } + aggregate.checkPendingJobParticipantStatus() + }() + return nil +} + +// handleJobRejection marks the job as rejected +func (aggregate *JobAggregate) handleJobRejection(siteUUID string) error { + participantInstance, err := aggregate.ParticipantRepo.GetByJobAndSiteUUID(aggregate.Job.UUID, siteUUID) + if err != nil { + return errors.Wrap(err, "failed to query participant") + } + participant := participantInstance.(*entity.JobParticipant) + + participant.Repo = aggregate.ParticipantRepo + if err := participant.UpdateStatus(entity.JobParticipantStatusRejected); err != nil { + return err + } + if err := aggregate.Job.UpdateStatus(entity.JobStatusRejected); err != nil { + return err + } + return nil +} + +// HandleJobApprovalResponse process job approval response +func (aggregate *JobAggregate) HandleJobApprovalResponse(siteUUID string, approved bool) error { + if approved { + return aggregate.handleJobApproval(siteUUID) + } else { + return aggregate.handleJobRejection(siteUUID) + } +} + +// RefreshJob checks the FATE job status +func (aggregate *JobAggregate) RefreshJob() error { + return aggregate.Job.CheckFATEJobStatus() +} + +// HandleJobStatusUpdate process job status update. If the job becomes running, then a monitoring routine will be started +func (aggregate *JobAggregate) HandleJobStatusUpdate(newJobStatus *entity.Job, participantStatusMap map[string]entity.JobParticipantStatus) error { + for siteUUID, newStatus := range participantStatusMap { + participant, ok := aggregate.Participants[siteUUID] + if ok && participant.Status != newStatus { + log.Info().Str("participant name", participant.SiteName). + Str("job name", aggregate.Job.Name).Str("job uuid", aggregate.Job.UUID). + Msgf("HandleJobStatusUpdate: change participant status") + if err := participant.UpdateStatus(newStatus); err != nil { + log.Err(err).Str("participant_uuid", siteUUID).Send() + } + } + } + if err := aggregate.Job.Update(newJobStatus); err != nil { + return err + } + if aggregate.Job.Status == entity.JobStatusSucceeded { + log.Info().Msgf("try to update result info for job: %s(%s)", aggregate.Job.Name, aggregate.Job.UUID) + go aggregate.updateJobResultInfo() + } + return nil +} + +// createOrUpdateParticipant is a helper function to create a pending participant or update an existing one's status of pending +func (aggregate *JobAggregate) createOrUpdateParticipant(participant *entity.JobParticipant) error { + participant.JobUUID = aggregate.Job.UUID + participantInstance, err := aggregate.ParticipantRepo.GetByJobAndSiteUUID(aggregate.Job.UUID, participant.SiteUUID) + if err != nil { + if errors.Is(err, repo.ErrJobParticipantNotFound) { + if err := participant.Create(); err != nil { + return errors.Wrapf(err, "failed to create participant: %s", participant.SiteUUID) + } + } else { + return errors.Wrap(err, "failed to query participant") + } + } else { + participant = participantInstance.(*entity.JobParticipant) + log.Info().Msgf("changing participant %s(%s) status to pending for job %s(%s)", participant.SiteName, participant.SiteUUID, aggregate.Job.Name, aggregate.Job.UUID) + participant.Repo = aggregate.ParticipantRepo + participant.Status = entity.JobParticipantStatusPending + if err := participant.Repo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrap(err, "failed to update participant status") + } + } + return nil +} + +// sendApprovedJobStatusUpdate sends an approved job status(running, succeeded, failed, etc) to FML manager +func (aggregate *JobAggregate) sendApprovedJobStatusUpdate() { + statusUpdateContext := fmlmanager.JobStatusUpdateContext{ + Status: uint8(aggregate.Job.Status), + StatusMessage: aggregate.Job.StatusMessage, + FATEJobID: aggregate.Job.FATEJobID, + FATEJobStatus: aggregate.Job.FATEJobStatus, + FATEModelID: aggregate.Job.FATEModelID, + FATEModelVersion: aggregate.Job.FATEModelVersion, + ParticipantStatusMap: map[string]uint8{}, + } + + for siteUUID := range aggregate.Participants { + statusUpdateContext.ParticipantStatusMap[siteUUID] = uint8(entity.JobParticipantStatusApproved) + } + client := fmlmanager.NewFMLManagerClient(aggregate.FMLManagerConnectionInfo.Endpoint, aggregate.FMLManagerConnectionInfo.ServerName) + if err := client.SendJobStatusUpdate(aggregate.Job.UUID, statusUpdateContext); err != nil { + log.Err(err).Str("job uuid", aggregate.Job.UUID).Msgf("failed to send job status update to FATE") + } +} + +// updateJobResultInfo updates job result +func (aggregate *JobAggregate) updateJobResultInfo() { + if aggregate.Job.Status != entity.JobStatusSucceeded { + return + } + role := aggregate.Initiator.SiteRole + partyID := aggregate.Initiator.SitePartyID + if participant, ok := aggregate.Participants[aggregate.JobContext.CurrentSiteUUID]; ok { + role = participant.SiteRole + partyID = participant.SitePartyID + } + + if err := aggregate.Job.UpdateResultInfo(partyID, role); err != nil { + log.Err(err).Str("job uuid", aggregate.Job.UUID).Msg("failed to update job result") + } +} + +// checkPendingJobParticipantStatus checks the participant status and may start the job +func (aggregate *JobAggregate) checkPendingJobParticipantStatus() { + // only the initiator can start the job + if aggregate.Job.Status != entity.JobStatusPending || aggregate.JobContext.CurrentSiteUUID != aggregate.Job.InitiatingSiteUUID { + return + } + + for _, participant := range aggregate.Participants { + if participant.Status != entity.JobParticipantStatusApproved { + log.Info().Str("job uuid", aggregate.Job.UUID).Msgf("job still pending on site %s(%s)", participant.SiteName, participant.SiteUUID) + return + } + } + log.Info().Str("job uuid", aggregate.Job.UUID).Msgf("job approved by all site, starting") + + // we firstly send the job status update to let other sites know the job has been approved and starts running + // then after the job is finished, we, in the callback, send the update again to other sites to update the job status and model info + // the initiating site will get the result in the callback + // and the other sites will start to get the result when handling the status update + if err := aggregate.Job.SubmitToFATE(func() { + aggregate.sendApprovedJobStatusUpdate() + aggregate.updateJobResultInfo() + }); err != nil { + log.Err(err).Str("job uuid", aggregate.Job.UUID).Msgf("failed to submit job to FATE") + } + aggregate.sendApprovedJobStatusUpdate() +} + +// reloadParticipant is a helper function to retrieve the participant info from the repo +func (aggregate *JobAggregate) reloadParticipant() error { + participantListInstance, err := aggregate.ParticipantRepo.GetListByJobUUID(aggregate.Job.UUID) + if err != nil { + return err + } + participantList := participantListInstance.([]entity.JobParticipant) + for index, participant := range participantList { + participantList[index].Repo = aggregate.ParticipantRepo + if participant.SiteUUID != aggregate.Job.InitiatingSiteUUID { + aggregate.Participants[participant.SiteUUID] = &participantList[index] + } + } + return nil +} + +// GetDataResultDownloadRequest returns a request object to be used to download the result data +func (aggregate *JobAggregate) GetDataResultDownloadRequest() (*http.Request, error) { + var componentName string + if aggregate.Job.Type == entity.JobTypePredict { + componentName = aggregate.Job.AlgorithmComponentName + } else if aggregate.Job.Type == entity.JobTypePSI { + componentName = "intersection_0" + } else { + return nil, errors.New("job is not a predicting job nor a PSI job") + } + if aggregate.Job.Status != entity.JobStatusSucceeded { + return nil, errors.New("job did not finished successfully") + } + role := aggregate.Initiator.SiteRole + partyID := aggregate.Initiator.SitePartyID + if participant, ok := aggregate.Participants[aggregate.JobContext.CurrentSiteUUID]; ok { + role = participant.SiteRole + partyID = participant.SitePartyID + } + + fateClient := fateclient.NewFATEFlowClient(aggregate.Job.FATEFlowContext.FATEFlowHost, + aggregate.Job.FATEFlowContext.FATEFlowPort, aggregate.Job.FATEFlowContext.FATEFlowIsHttps) + return fateClient.GetComponentOutputDataDownloadRequest(fateclient.ComponentTrackingCommonRequest{ + JobID: aggregate.Job.FATEJobID, + PartyID: partyID, + Role: string(role), + ComponentName: componentName, + }) +} diff --git a/site-portal/server/domain/aggregate/job_aggregate_test.go b/site-portal/server/domain/aggregate/job_aggregate_test.go new file mode 100644 index 00000000..ef741e9e --- /dev/null +++ b/site-portal/server/domain/aggregate/job_aggregate_test.go @@ -0,0 +1,93 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregate + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/stretchr/testify/assert" +) + +func getJobAggregate() *JobAggregate { + return &JobAggregate{ + Initiator: &entity.JobParticipant{ + SitePartyID: 1, + DataTableName: "guest-tablename-0", + DataTableNamespace: "guest-tablens-0", + }, + Participants: map[string]*entity.JobParticipant{ + "hostuuid1": { + SitePartyID: 2, + DataTableName: "host-tablename-0", + DataTableNamespace: "host-tablens-0"}, + "hostuuid2": { + SitePartyID: 3, + DataTableName: "host-tablename-1", + DataTableNamespace: "host-tablens-1"}, + }, + } +} + +func TestGenerateReaderConfigMaps(t *testing.T) { + jobAggregate := getJobAggregate() + hostUuidList := []string{"hostuuid1", "hostuuid2"} + hostMap, guestMap := jobAggregate.GenerateReaderConfigMaps(hostUuidList) + expectedHostMap := map[string]map[string]map[string]map[string]string{ + "0": { + "reader_0": { + "table": { + "name": "host-tablename-0", + "namespace": "host-tablens-0", + }, + }, + }, + "1": { + "reader_0": { + "table": { + "name": "host-tablename-1", + "namespace": "host-tablens-1", + }, + }, + }, + } + expectedGuestMap := map[string]map[string]map[string]map[string]string{ + "0": { + "reader_0": { + "table": { + "name": "guest-tablename-0", + "namespace": "guest-tablens-0", + }, + }, + }, + } + assert.Equal(t, fmt.Sprintln(expectedGuestMap), fmt.Sprintln(guestMap)) + assert.Equal(t, fmt.Sprintln(expectedHostMap), fmt.Sprintln(hostMap)) +} + +func TestGenerateGeneralTrainingConf(t *testing.T) { + jobAggregate := getJobAggregate() + // Make below list in the reverser order + hostUuidList := []string{"hostuuid2", "hostuuid1"} + actualRes, _ := jobAggregate.GenerateGeneralTrainingConf(hostUuidList) + actualResStruct := map[string]interface{}{} + json.Unmarshal([]byte(actualRes), &actualResStruct) + role := actualResStruct["role"].(map[string]interface{}) + host := role["host"].([]interface{}) + // hostUuid2 matches party id 3 + assert.Equal(t, int(host[0].(float64)), 3) +} diff --git a/site-portal/server/domain/aggregate/project_aggregate.go b/site-portal/server/domain/aggregate/project_aggregate.go new file mode 100644 index 00000000..ed46ad25 --- /dev/null +++ b/site-portal/server/domain/aggregate/project_aggregate.go @@ -0,0 +1,777 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregate + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fmlmanager" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// ProjectAggregate is the aggregation of the concept of a "project" +// We tried to follow the practice suggested by many DDD articles, however we made +// some simplification - we typically only manipulate with only one "project data" +// or one "project participant" for a project. And if there are bulk changes, we +// simply work with the "repo" to persist the change as there is no special business +// logics to construct a complete "project". +type ProjectAggregate struct { + Project *entity.Project + Participant *entity.ProjectParticipant + ProjectData *entity.ProjectData + ProjectRepo repo.ProjectRepository + ParticipantRepo repo.ProjectParticipantRepository + InvitationRepo repo.ProjectInvitationRepository + DataRepo repo.ProjectDataRepository +} + +// ProjectInvitationContext is the context we use to issue project invitation +type ProjectInvitationContext struct { + FMLManagerConnectionInfo *FMLManagerConnectionInfo + SiteUUID string + SitePartyID uint + SiteName string + SiteDescription string +} + +// ProjectLocalDataAssociationContext is the context we use to create new local data association +type ProjectLocalDataAssociationContext struct { + FMLManagerConnectionInfo *FMLManagerConnectionInfo + LocalData *entity.ProjectData +} + +// ProjectLocalDataDismissalContext is the context we use to dismiss data association +type ProjectLocalDataDismissalContext struct { + FMLManagerConnectionInfo *FMLManagerConnectionInfo +} + +// ProjectRemoteDataAssociationContext is the context we use to create remote data association +type ProjectRemoteDataAssociationContext struct { + LocalSiteUUID string + RemoteDataList []entity.ProjectData +} + +// ProjectRemoteDataDismissalContext is the context we use to dismiss remote data association +type ProjectRemoteDataDismissalContext struct { + LocalSiteUUID string + RemoteDataUUIDList []string +} + +// ProjectSyncContext is the context we use to sync project info with fml-manager +type ProjectSyncContext struct { + FMLManagerConnectionInfo *FMLManagerConnectionInfo + LocalSiteUUID string +} + +// CountParticipant returns number of participants in the current project +func (aggregate *ProjectAggregate) CountParticipant() (int64, error) { + if num, err := aggregate.ParticipantRepo.CountJoinedParticipantByProjectUUID(aggregate.Project.UUID); err != nil { + return 0, err + } else { + // add the manager too + return num + 1, nil + } +} + +// ListParticipant returns participants of the current project, or all participant in FML manager +func (aggregate *ProjectAggregate) ListParticipant(all bool, fmlManagerConnectionInfo *FMLManagerConnectionInfo) ([]entity.ProjectParticipant, error) { + instanceList, err := aggregate.ParticipantRepo.GetByProjectUUID(aggregate.Project.UUID) + if err != nil { + return nil, err + } + allParticipants := instanceList.([]entity.ProjectParticipant) + var participantList []entity.ProjectParticipant + for _, participant := range allParticipants { + if participant.Status == entity.ProjectParticipantStatusOwner || + participant.Status == entity.ProjectParticipantStatusJoined || + participant.Status == entity.ProjectParticipantStatusPending { + participantList = append(participantList, participant) + } + } + if !all { + return participantList, nil + } + if !fmlManagerConnectionInfo.Connected || fmlManagerConnectionInfo.Endpoint == "" { + return nil, errors.Errorf("not connected to FML manager") + } + uuidMap := make(map[string]interface{}) + for _, participant := range participantList { + uuidMap[participant.SiteUUID] = nil + } + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + siteList, err := client.GetAllSite() + if err != nil { + return nil, errors.Wrapf(err, "unable to query site list from FML manager") + } + for _, site := range siteList { + if _, ok := uuidMap[site.UUID]; !ok { + participantList = append(participantList, entity.ProjectParticipant{ + Model: gorm.Model{}, + UUID: "", + ProjectUUID: aggregate.Project.UUID, + SiteUUID: site.UUID, + SiteName: site.Name, + SitePartyID: site.PartyID, + SiteDescription: site.Description, + Status: entity.ProjectParticipantStatusUnknown, + }) + } + } + return participantList, nil +} + +// InviteParticipant send project invitation to certain site +func (aggregate *ProjectAggregate) InviteParticipant(invitationContext *ProjectInvitationContext) error { + if !invitationContext.FMLManagerConnectionInfo.Connected { + return errors.New("FML manager not connected") + } + if aggregate.Project.Type == entity.ProjectTypeRemote { + return errors.New("project not managed by current site") + } + if aggregate.Project.ManagingSiteUUID == invitationContext.SiteUUID { + return errors.Errorf("invalid targeting site uuid %s", invitationContext.SiteUUID) + } + + var dataList []entity.ProjectData + if aggregate.Project.Type == entity.ProjectTypeLocal { + log.Info().Msgf("building local data list for creating the project to FML Manager, project: %s(%s)", aggregate.Project.Name, aggregate.Project.UUID) + aggregate.Project.Type = entity.ProjectTypeFederatedLocal + dataListInstance, err := aggregate.DataRepo.GetListByProjectUUID(aggregate.Project.UUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList = dataListInstance.([]entity.ProjectData) + } + // create invitation + invitation := &entity.ProjectInvitation{ + UUID: uuid.NewV4().String(), + ProjectUUID: aggregate.Project.UUID, + SiteUUID: invitationContext.SiteUUID, + Status: entity.ProjectInvitationStatusCreated, + } + if err := aggregate.InvitationRepo.Create(invitation); err != nil { + return err + } + // send invitation to fml manager + client := fmlmanager.NewFMLManagerClient(invitationContext.FMLManagerConnectionInfo.Endpoint, invitationContext.FMLManagerConnectionInfo.ServerName) + associatedDataList := make([]fmlmanager.ProjectDataAssociation, len(dataList)) + for index, localData := range dataList { + associatedDataList[index] = fmlmanager.ProjectDataAssociation{ + ProjectDataAssociationBase: fmlmanager.ProjectDataAssociationBase{ + DataUUID: localData.DataUUID, + }, + Name: localData.Name, + Description: localData.Description, + SiteName: localData.SiteName, + SiteUUID: localData.SiteUUID, + SitePartyID: localData.SitePartyID, + TableName: localData.TableName, + TableNamespace: localData.TableNamespace, + CreationTime: localData.CreationTime, + UpdateTime: localData.UpdateTime, + } + } + if err := client.SendInvitation(fmlmanager.ProjectInvitation{ + UUID: invitation.UUID, + SiteUUID: invitationContext.SiteUUID, + SitePartyID: invitationContext.SitePartyID, + ProjectUUID: aggregate.Project.UUID, + ProjectName: aggregate.Project.Name, + ProjectDescription: aggregate.Project.Description, + ProjectAutoApprovalEnabled: aggregate.Project.AutoApprovalEnabled, + ProjectManager: aggregate.Project.Manager, + ProjectManagingSiteName: aggregate.Project.ManagingSiteName, + ProjectManagingSitePartyID: aggregate.Project.ManagingSitePartyID, + ProjectManagingSiteUUID: aggregate.Project.ManagingSiteUUID, + AssociatedData: associatedDataList, + }); err != nil { + log.Err(err).Msg("failed to send invitation to FML manager") + return err + } + // create/update participant item + // XXX: site name and party ID info should be queried from FML manager + participant := &entity.ProjectParticipant{ + UUID: uuid.NewV4().String(), + ProjectUUID: aggregate.Project.UUID, + SiteUUID: invitationContext.SiteUUID, + SiteName: invitationContext.SiteName, + SitePartyID: invitationContext.SitePartyID, + SiteDescription: invitationContext.SiteDescription, + Status: entity.ProjectParticipantStatusPending, + } + if err := aggregate.CreateOrUpdateParticipant(participant); err != nil { + return err + } + // update status + invitation.Status = entity.ProjectInvitationStatusSent + if err := aggregate.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + // update project type + if err := aggregate.ProjectRepo.UpdateTypeByUUID(aggregate.Project); err != nil { + return errors.Wrapf(err, "failed to update project type") + } + return nil +} + +// ProcessInvitation handle's invitation request from FML manager and saves it into repo +func (aggregate *ProjectAggregate) ProcessInvitation(invitation *entity.ProjectInvitation, siteUUID string) error { + if invitation.SiteUUID != siteUUID { + return errors.Errorf("target site uuid: %s is not current site: %s", invitation.UUID, siteUUID) + } + aggregate.Project.Status = entity.ProjectStatusPending + aggregate.Project.Type = entity.ProjectTypeRemote + if err := aggregate.CreateOrUpdateProject(); err != nil { + return err + } + if err := aggregate.ParticipantRepo.DeleteByProjectUUID(aggregate.Project.UUID); err != nil { + return errors.Wrapf(err, "failed to clear participants") + } + if err := aggregate.DataRepo.DeleteByProjectUUID(aggregate.Project.UUID); err != nil { + return errors.Wrap(err, "failed to clear project data") + } + // create invitation + if invitation.UUID == "" { + return errors.New("invalid invitation") + } + invitation.Status = entity.ProjectInvitationStatusSent + if err := aggregate.InvitationRepo.Create(invitation); err != nil { + return err + } + return nil +} + +// CreateOrUpdateProject creates the project in the repo or update its status +func (aggregate *ProjectAggregate) CreateOrUpdateProject() error { + projectInstance, err := aggregate.ProjectRepo.GetByUUID(aggregate.Project.UUID) + if err != nil { + if errors.Is(err, repo.ErrProjectNotFound) { + if err := aggregate.Project.Create(); err != nil { + return errors.Wrapf(err, "failed to create project") + } + } else { + return err + } + } else { + project := projectInstance.(*entity.Project) + project.Status = aggregate.Project.Status + aggregate.Project = project + if err := aggregate.ProjectRepo.UpdateStatusByUUID(project); err != nil { + return errors.Wrapf(err, "failed to update project info") + } + } + return nil +} + +// CreateOrUpdateParticipant creates a participant record in the repo or update its status +func (aggregate *ProjectAggregate) CreateOrUpdateParticipant(newParticipant *entity.ProjectParticipant) error { + instance, err := aggregate.ParticipantRepo.GetByProjectAndSiteUUID(aggregate.Project.UUID, newParticipant.SiteUUID) + if err != nil { + if errors.Is(err, repo.ErrProjectParticipantNotFound) { + newParticipant.Model = gorm.Model{} + if newParticipant.UUID == "" { + newParticipant.UUID = uuid.NewV4().String() + } + if err := aggregate.ParticipantRepo.Create(newParticipant); err != nil { + return errors.Wrapf(err, "failed to create participant info") + } + } else { + return errors.Wrapf(err, "failed to query participant info") + } + } else { + participant := instance.(*entity.ProjectParticipant) + participant.Status = newParticipant.Status + if err := aggregate.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update participant info") + } + } + return nil +} + +// JoinProject joins the project by sending invitation response +func (aggregate *ProjectAggregate) JoinProject(fmlManagerConnectionInfo *FMLManagerConnectionInfo) error { + if !fmlManagerConnectionInfo.Connected || fmlManagerConnectionInfo.Endpoint == "" { + return errors.Errorf("not connected to FML manager") + } + if aggregate.Project.Status != entity.ProjectStatusPending { + return errors.Errorf("invalide project status: %d", aggregate.Project.Status) + } + aggregate.Project.Status = entity.ProjectStatusJoined + invitationInstance, err := aggregate.InvitationRepo.GetByProjectUUID(aggregate.Project.UUID) + if err != nil { + return err + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusAccepted + // send join request to fml manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendInvitationAcceptance(invitation.UUID); err != nil { + return errors.Wrapf(err, "unable to send invitation response to FML manager") + } + // update project and invitation status + if err := aggregate.ProjectRepo.UpdateStatusByUUID(aggregate.Project); err != nil { + return err + } + if err := aggregate.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + return nil +} + +// RejectProject reject to join the project by sending the invitation response +func (aggregate *ProjectAggregate) RejectProject(fmlManagerConnectionInfo *FMLManagerConnectionInfo) error { + if !fmlManagerConnectionInfo.Connected || fmlManagerConnectionInfo.Endpoint == "" { + return errors.Errorf("not connected to FML manager") + } + if aggregate.Project.Status != entity.ProjectStatusPending { + return errors.Errorf("invalide project status: %d", aggregate.Project.Status) + } + aggregate.Project.Status = entity.ProjectStatusRejected + invitationInstance, err := aggregate.InvitationRepo.GetByProjectUUID(aggregate.Project.UUID) + if err != nil { + return err + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRejected + // send reject request to fml manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendInvitationRejection(invitation.UUID); err != nil { + return errors.Wrapf(err, "unable to send invitation response to FML manager") + } + // update project and invitation status + if err := aggregate.ProjectRepo.UpdateStatusByUUID(aggregate.Project); err != nil { + return err + } + if err := aggregate.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + return nil +} + +// LeaveProject leave the current remote project +func (aggregate *ProjectAggregate) LeaveProject(fmlManagerConnectionInfo *FMLManagerConnectionInfo) error { + // sanity checks + if aggregate.Project.Type != entity.ProjectTypeRemote { + return errors.New("project is not managed by other site") + } + if aggregate.Project.Status != entity.ProjectStatusJoined || aggregate.Participant.Status != entity.ProjectParticipantStatusJoined { + return errors.New("current site is not in the project") + } + if !fmlManagerConnectionInfo.Connected || fmlManagerConnectionInfo.Endpoint == "" { + return errors.Errorf("not connected to FML manager") + } + + // no data association should exist + dataListInstance, err := aggregate.DataRepo.GetListByProjectAndSiteUUID(aggregate.Project.UUID, aggregate.Participant.SiteUUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + return errors.Errorf("at least one data association exists, data: %s", data.Name) + } + } + + // send leaving event to fml manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendProjectParticipantLeaving(aggregate.Project.UUID, aggregate.Participant.SiteUUID); err != nil { + return errors.Wrapf(err, "error sending participant leaving request to FML manager") + } + + // update project and participant status + aggregate.Project.Status = entity.ProjectStatusLeft + if err := aggregate.ProjectRepo.UpdateStatusByUUID(aggregate.Project); err != nil { + return err + } + aggregate.Participant.Status = entity.ProjectParticipantStatusLeft + if err := aggregate.ParticipantRepo.UpdateStatusByUUID(aggregate.Participant); err != nil { + return err + } + return nil +} + +// CreateRemoteProjectParticipants adds the passed participants into the repo +func (aggregate *ProjectAggregate) CreateRemoteProjectParticipants(participants []entity.ProjectParticipant) error { + for _, participant := range participants { + participant.Model.ID = 0 + if err := aggregate.CreateOrUpdateParticipant(&participant); err != nil { + return err + } + } + return nil +} + +// RemoveParticipant removes a joined participant or revoke an invitation to a pending site +func (aggregate *ProjectAggregate) RemoveParticipant(siteUUID string, fmlManagerConnectionInfo *FMLManagerConnectionInfo) error { + if !fmlManagerConnectionInfo.Connected { + return errors.New("FML manager not connected") + } + if aggregate.Project.Type == entity.ProjectTypeRemote { + return errors.New("project not managed by current site") + } + + // find participant info + instance, err := aggregate.ParticipantRepo.GetByProjectAndSiteUUID(aggregate.Project.UUID, siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to query participant info") + } + participant := instance.(*entity.ProjectParticipant) + + if participant.Status == entity.ProjectParticipantStatusPending { + log.Info().Msgf("revoke invitation for participant: %s", siteUUID) + participant.Status = entity.ProjectParticipantStatusRevoked + // find invitation + invitationInstance, err := aggregate.InvitationRepo.GetByProjectAndSiteUUID(aggregate.Project.UUID, siteUUID) + if err != nil { + return errors.Wrapf(err, "failed to get invitation") + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRevoked + + // send revocation request to FML manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendInvitationRevocation(invitation.UUID); err != nil { + return errors.Wrapf(err, "unable to send invitation revocation to FML manager") + } + + // update status in the repo + if err := aggregate.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update participant status") + } + if err := aggregate.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return errors.Wrapf(err, "failed to update invitation status") + } + } else if participant.Status == entity.ProjectParticipantStatusJoined { + log.Info().Msgf("dismiss participant: %s", siteUUID) + participant.Status = entity.ProjectParticipantStatusDismissed + // send dismissal request to FML manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendProjectParticipantDismissal(aggregate.Project.UUID, participant.SiteUUID); err != nil { + return errors.Wrapf(err, "unable to send participant dismissal to FML manager") + } + + // dismiss data association + dataListInstance, err := aggregate.DataRepo.GetListByProjectAndSiteUUID(aggregate.Project.UUID, siteUUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + data.Status = entity.ProjectDataStatusDismissed + if err := aggregate.DataRepo.UpdateStatusByUUID(&data); err != nil { + return errors.Wrapf(err, "failed to dismiss data %s from site: %s", data.Name, participant.SiteName) + } + } + } + + // update participant status in the repo + if err := aggregate.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return errors.Wrapf(err, "failed to update participant status") + } + } else { + return errors.Errorf("invalid participant status: %d", participant.Status) + } + + return nil +} + +// AssociateLocalData associates local data into the project +func (aggregate *ProjectAggregate) AssociateLocalData(localDataAssociationCtx *ProjectLocalDataAssociationContext) error { + if aggregate.Project.Type == entity.ProjectTypeRemote || aggregate.Project.Type == entity.ProjectTypeFederatedLocal { + if !localDataAssociationCtx.FMLManagerConnectionInfo.Connected { + return errors.New("project contains other parties but FML manager is not connected") + } + client := fmlmanager.NewFMLManagerClient(localDataAssociationCtx.FMLManagerConnectionInfo.Endpoint, localDataAssociationCtx.FMLManagerConnectionInfo.ServerName) + if err := client.SendProjectDataAssociation(aggregate.Project.UUID, fmlmanager.ProjectDataAssociation{ + ProjectDataAssociationBase: fmlmanager.ProjectDataAssociationBase{ + DataUUID: localDataAssociationCtx.LocalData.DataUUID, + }, + Name: localDataAssociationCtx.LocalData.Name, + Description: localDataAssociationCtx.LocalData.Description, + SiteName: localDataAssociationCtx.LocalData.SiteName, + SiteUUID: localDataAssociationCtx.LocalData.SiteUUID, + SitePartyID: localDataAssociationCtx.LocalData.SitePartyID, + TableName: localDataAssociationCtx.LocalData.TableName, + TableNamespace: localDataAssociationCtx.LocalData.TableNamespace, + CreationTime: localDataAssociationCtx.LocalData.CreationTime, + UpdateTime: localDataAssociationCtx.LocalData.UpdateTime, + }); err != nil { + return errors.Wrap(err, "failed to send project data association to FML manager") + } + } + localDataAssociationCtx.LocalData.Type = entity.ProjectDataTypeLocal + localDataAssociationCtx.LocalData.Status = entity.ProjectDataStatusAssociated + if err := aggregate.CreateOrUpdateData(localDataAssociationCtx.LocalData); err != nil { + return errors.Wrapf(err, "failed to create project data") + } + return nil +} + +// CreateOrUpdateData creates the associated data record or updates its status +func (aggregate *ProjectAggregate) CreateOrUpdateData(newData *entity.ProjectData) error { + instance, err := aggregate.DataRepo.GetByProjectAndDataUUID(aggregate.Project.UUID, newData.DataUUID) + if err != nil { + if errors.Is(err, repo.ErrProjectDataNotFound) { + newData.Model.ID = 0 + if newData.UUID == "" { + newData.UUID = uuid.NewV4().String() + } + if err := aggregate.DataRepo.Create(newData); err != nil { + return errors.Wrapf(err, "failed to create data association") + } + } else { + return errors.Wrapf(err, "failed to query data association") + } + } else { + data := instance.(*entity.ProjectData) + data.Status = newData.Status + if err := aggregate.DataRepo.UpdateStatusByUUID(data); err != nil { + return errors.Wrapf(err, "failed to update data association") + } + } + return nil +} + +// DismissAssociatedLocalData dismisses local data association +func (aggregate *ProjectAggregate) DismissAssociatedLocalData(context *ProjectLocalDataDismissalContext) error { + if aggregate.Project.Type == entity.ProjectTypeRemote || aggregate.Project.Type == entity.ProjectTypeFederatedLocal { + if !context.FMLManagerConnectionInfo.Connected { + return errors.New("project contains other parties but FML manager is not connected") + } + client := fmlmanager.NewFMLManagerClient(context.FMLManagerConnectionInfo.Endpoint, context.FMLManagerConnectionInfo.ServerName) + if err := client.SendProjectDataDismissal(aggregate.Project.UUID, fmlmanager.ProjectDataAssociationBase{ + DataUUID: aggregate.ProjectData.DataUUID, + }); err != nil { + return errors.Wrap(err, "failed to send project data dismissal to FML manager") + } + } + if aggregate.ProjectData.Type != entity.ProjectDataTypeLocal { + return errors.New("cannot dismiss data from other sites") + } + aggregate.ProjectData.Status = entity.ProjectDataStatusDismissed + if err := aggregate.DataRepo.UpdateStatusByUUID(aggregate.ProjectData); err != nil { + return errors.Wrapf(err, "failed to dismiss project data") + } + return nil +} + +// CreateRemoteProjectData creates remote data association +func (aggregate *ProjectAggregate) CreateRemoteProjectData(context *ProjectRemoteDataAssociationContext) error { + for _, data := range context.RemoteDataList { + if data.SiteUUID == context.LocalSiteUUID { + log.Warn().Str("data uuid", data.UUID).Msgf("data from this local site") + continue + } + data.Type = entity.ProjectDataTypeRemote + data.Status = entity.ProjectDataStatusAssociated + err := aggregate.CreateOrUpdateData(&data) + if err != nil { + return err + } + } + return nil +} + +// DeleteRemoteProjectData deletes remote data association +func (aggregate *ProjectAggregate) DeleteRemoteProjectData(context *ProjectRemoteDataDismissalContext) error { + for _, dataUUID := range context.RemoteDataUUIDList { + if dataUUID == context.LocalSiteUUID { + log.Warn().Str("data uuid", dataUUID).Msgf("data from this local site") + continue + } + instance, err := aggregate.DataRepo.GetByProjectAndDataUUID(aggregate.Project.UUID, dataUUID) + if err != nil { + if errors.Is(err, repo.ErrProjectDataNotFound) { + log.Warn().Str("data uuid", dataUUID).Str("project uuid", aggregate.Project.UUID).Msg("data not associated in this project") + continue + } else { + return errors.Wrapf(err, "failed to query data association") + } + } else { + data := instance.(*entity.ProjectData) + data.Status = entity.ProjectDataStatusDismissed + if err := aggregate.DataRepo.UpdateStatusByUUID(data); err != nil { + return errors.Wrapf(err, "failed to update data association") + } + } + } + return nil +} + +// SyncDataAssociation sync the data association info with the fml manager +func (aggregate *ProjectAggregate) SyncDataAssociation(context *ProjectSyncContext) error { + if aggregate.Project.Type == entity.ProjectTypeLocal { + return nil + } + if !context.FMLManagerConnectionInfo.Connected { + log.Warn().Msg("SyncDataAssociation: FML manager is not connected") + return nil + } + log.Info().Msgf("start syncing project data, project: %s(%s)", aggregate.Project.Name, aggregate.Project.UUID) + client := fmlmanager.NewFMLManagerClient(context.FMLManagerConnectionInfo.Endpoint, context.FMLManagerConnectionInfo.ServerName) + associatedDataMap, err := client.GetProjectDataAssociation(aggregate.Project.UUID) + if err != nil { + return err + } + + dataListInstance, err := aggregate.DataRepo.GetListByProjectUUID(aggregate.Project.UUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + for _, data := range dataList { + oldStatus := data.Status + if _, ok := associatedDataMap[data.DataUUID]; ok { + data.Status = entity.ProjectDataStatusAssociated + delete(associatedDataMap, data.DataUUID) + } else { + data.Status = entity.ProjectDataStatusDismissed + } + if oldStatus != data.Status { + log.Warn().Msgf("changing stale data association status, data: %v", data) + if err := aggregate.DataRepo.UpdateStatusByUUID(&data); err != nil { + return errors.Wrapf(err, "failed to update data association") + } + } + } + for _, associatedData := range associatedDataMap { + data := &entity.ProjectData{ + Name: associatedData.Name, + Description: associatedData.Description, + ProjectUUID: aggregate.Project.UUID, + DataUUID: associatedData.DataUUID, + SiteUUID: associatedData.SiteUUID, + SiteName: associatedData.SiteName, + SitePartyID: associatedData.SitePartyID, + Type: entity.ProjectDataTypeRemote, + Status: entity.ProjectDataStatusAssociated, + TableName: associatedData.TableName, + TableNamespace: associatedData.TableNamespace, + CreationTime: associatedData.CreationTime, + UpdateTime: associatedData.UpdateTime, + Repo: aggregate.DataRepo, + } + if associatedData.SiteUUID == context.LocalSiteUUID { + data.Type = entity.ProjectDataTypeLocal + } + log.Warn().Msgf("adding or updating missing data association: %v", data) + if err := aggregate.CreateOrUpdateData(data); err != nil { + return err + } + } + return nil +} + +// SyncParticipant sync the participant status from fml manager +func (aggregate *ProjectAggregate) SyncParticipant(context *ProjectSyncContext) error { + if aggregate.Project.Type == entity.ProjectTypeLocal { + return nil + } + if !context.FMLManagerConnectionInfo.Connected { + log.Warn().Msg("SyncParticipant: FML manager is not connected") + return nil + } + log.Info().Msgf("start syncing project participants, project: %s(%s)", aggregate.Project.Name, aggregate.Project.UUID) + client := fmlmanager.NewFMLManagerClient(context.FMLManagerConnectionInfo.Endpoint, context.FMLManagerConnectionInfo.ServerName) + participantMap, err := client.GetProjectParticipant(aggregate.Project.UUID) + if err != nil { + return err + } + + participantListInstance, err := aggregate.ParticipantRepo.GetByProjectUUID(aggregate.Project.UUID) + if err != nil { + return err + } + participantList := participantListInstance.([]entity.ProjectParticipant) + + for _, participant := range participantList { + oldStatus := participant.Status + if _, ok := participantMap[participant.SiteUUID]; ok { + participant.Status = entity.ProjectParticipantStatus(participantMap[participant.SiteUUID].Status) + // change pending participant status to dismissed for non-managing site + if aggregate.Project.ManagingSiteUUID != context.LocalSiteUUID && participant.Status == entity.ProjectParticipantStatusPending { + participant.Status = entity.ProjectParticipantStatusDismissed + } + delete(participantMap, participant.SiteUUID) + } else { + participant.Status = entity.ProjectParticipantStatusDismissed + } + if participant.Status != oldStatus { + log.Warn().Msgf("changing stale participant status, participant: %v", participant) + if err := aggregate.ParticipantRepo.UpdateStatusByUUID(&participant); err != nil { + return errors.Wrapf(err, "failed to update participant info") + } + } + } + + for _, participant := range participantMap { + newParticipant := &entity.ProjectParticipant{ + ProjectUUID: participant.ProjectUUID, + SiteUUID: participant.SiteUUID, + SiteName: participant.SiteName, + SitePartyID: participant.SitePartyID, + SiteDescription: participant.SiteDescription, + Status: entity.ProjectParticipantStatus(participant.Status), + } + // change pending participant status to dismissed for non-managing site + if aggregate.Project.ManagingSiteUUID != context.LocalSiteUUID && newParticipant.Status == entity.ProjectParticipantStatusPending { + newParticipant.Status = entity.ProjectParticipantStatusDismissed + } + log.Warn().Msgf("adding or updating missing participant: %v", participant) + if err := aggregate.CreateOrUpdateParticipant(newParticipant); err != nil { + return err + } + } + return nil +} + +// CloseProject closes the current project managed by current site +func (aggregate *ProjectAggregate) CloseProject(fmlManagerConnectionInfo *FMLManagerConnectionInfo) error { + if aggregate.Project.Type == entity.ProjectTypeRemote { + return errors.New("project is managed by other site") + } + + // work with fml manager to close the project + if aggregate.Project.Type == entity.ProjectTypeFederatedLocal { + if !fmlManagerConnectionInfo.Connected || fmlManagerConnectionInfo.Endpoint == "" { + return errors.Errorf("not connected to FML manager") + } + // send closing event to fml manager + client := fmlmanager.NewFMLManagerClient(fmlManagerConnectionInfo.Endpoint, fmlManagerConnectionInfo.ServerName) + if err := client.SendProjectClosing(aggregate.Project.UUID); err != nil { + return errors.Wrapf(err, "error sending project closing request to FML manager") + } + } + aggregate.Project.Status = entity.ProjectStatusClosed + if err := aggregate.ProjectRepo.UpdateStatusByUUID(aggregate.Project); err != nil { + return errors.Wrapf(err, "failed to update project status") + } + return nil +} diff --git a/site-portal/server/domain/entity/job.go b/site-portal/server/domain/entity/job.go new file mode 100644 index 00000000..9b049da1 --- /dev/null +++ b/site-portal/server/domain/entity/job.go @@ -0,0 +1,556 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/event" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// Job represents a FATE job +type Job struct { + gorm.Model + Name string `json:"name" gorm:"type:varchar(255)"` + Description string `json:"description" gorm:"type:text"` + UUID string `json:"uuid" gorm:"type:varchar(36)"` + ProjectUUID string `json:"project_uuid" gorm:"type:varchar(36)"` + Type JobType `json:"type"` + Status JobStatus `json:"status"` + StatusMessage string `gorm:"type:text"` + AlgorithmType JobAlgorithmType + AlgorithmComponentName string `json:"algorithm_component_name" gorm:"type:varchar(255)"` + EvaluateComponentName string `json:"evaluate_component_name" gorm:"type:varchar(255)"` + AlgorithmConfig valueobject.AlgorithmConfig `gorm:"type:text"` + ModelName string `json:"model_name" gorm:"type:varchar(255)"` + PredictingModelUUID string `gorm:"type:varchar(36)"` + InitiatingSiteUUID string `gorm:"type:varchar(36)"` + InitiatingSiteName string `gorm:"type:varchar(255)"` + InitiatingSitePartyID uint + InitiatingUser string `gorm:"type:varchar(255)"` + IsInitiatingSite bool + FATEJobID string `gorm:"type:varchar(255);column:fate_job_id"` + FATEJobStatus string `gorm:"type:varchar(36);column:fate_job_status"` + FATEModelID string `gorm:"type:varchar(255);column:fate_model_id"` + FATEModelVersion string `gorm:"type:varchar(255);column:fate_model_version"` + FinishedAt time.Time + ResultJson string `gorm:"type:text"` + Conf string `gorm:"type:text"` + DSL string `gorm:"type:text"` + RequestJson string `gorm:"type:text"` + FATEFlowContext FATEFlowContext `gorm:"-"` + Repo repo.JobRepository `gorm:"-"` +} + +// FATEFlowContext currently contains FATE flow connection info +type FATEFlowContext struct { + // FATEFlowHost is the host address of the service + FATEFlowHost string + // FATEFlowPort is the port of the service + FATEFlowPort uint + // FATEFlowIsHttps is whether the connection should be over TLS + FATEFlowIsHttps bool +} + +// JobStatus is the enum of job status +type JobStatus uint8 + +const ( + JobStatusUnknown JobStatus = iota + JobStatusPending + JobStatusRejected + JobStatusRunning + JobStatusFailed + JobStatusSucceeded + JobStatusDeploying + JobStatusDeleted +) + +func (s JobStatus) String() string { + names := map[JobStatus]string{ + JobStatusUnknown: "Unknown", + JobStatusPending: "Pending", + JobStatusRejected: "Rejected", + JobStatusRunning: "Running", + JobStatusFailed: "Failed", + JobStatusSucceeded: "Succeeded", + } + return names[s] +} + +// JobType is the enum of job type +type JobType uint8 + +const ( + JobTypeUnknown JobType = iota + JobTypeTraining + JobTypePredict + JobTypePSI +) + +func (t JobType) String() string { + names := map[JobType]string{ + JobTypeUnknown: "Unknown", + JobTypeTraining: "Modeling", + JobTypePredict: "Predict", + JobTypePSI: "PSI", + } + return names[t] +} + +// JobAlgorithmType is the enum of the job algorithm +type JobAlgorithmType uint8 + +const ( + JobAlgorithmTypeUnknown JobAlgorithmType = iota + JobAlgorithmTypeHomoLR + JobAlgorithmTypeHomoSBT +) + +const ( + jobResultModelEvaluation = "model_evaluation" + jobResultOutputData = "output_data" + jobResultOutputDataMeta = "output_meta" + jobResultComponentSummary = "component_summary" +) + +// Create initializes the job and save into the repo. The uuid will be automatically generated if not set +func (job *Job) Create() error { + job.Status = JobStatusPending + job.Model = gorm.Model{} + if job.UUID == "" { + job.UUID = uuid.NewV4().String() + } + if job.Type != JobTypePredict { + job.FATEModelID = "" + job.FATEModelVersion = "" + } + job.FATEJobID = "" + job.FATEJobStatus = "" + if err := job.Repo.Create(job); err != nil { + return err + } + return nil +} + +// Validate checks the job configuration +func (job *Job) Validate() error { + if job.Type == JobTypeTraining { + // XXX: this is a simple and strict check that training job must contain an evaluation component + if !strings.Contains(job.DSL, "Evaluation") { + return errors.New("training job must contain an Evaluation module") + } + if !(strings.Contains(job.DSL, "HomoLR") || + strings.Contains(job.DSL, "HomoSecureboost")) { + return errors.Errorf("training job must contain at least one algorithm component") + } + } else if job.Type == JobTypePredict { + if job.FATEModelID == "" || job.FATEModelVersion == "" { + return errors.New("predicting job must contain model id and model version") + } + } + return nil +} + +// SubmitToFATE submits the job to FATE system and starts a monitoring routine +func (job *Job) SubmitToFATE(finishCB func()) error { + if job.Conf == "" || job.DSL == "" { + return errors.New("no conf or dsl") + } + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + fateJobID, modelInfo, submissionErr := fateClient.SubmitJob(job.Conf, job.DSL) + if submissionErr != nil { + if err := job.UpdateStatus(JobStatusFailed); err != nil { + log.Err(err).Send() + } + if err := job.UpdateStatusMessage(fmt.Sprintf("failed to submit FATE job: %v", submissionErr)); err != nil { + log.Err(err).Send() + } + return errors.Wrap(submissionErr, "failed to submit job to FATE") + } + job.FATEJobID = fateJobID + job.FATEJobStatus = "started" + if job.Type != JobTypePredict { + job.FATEModelVersion = modelInfo.ModelVersion + job.FATEModelID = modelInfo.ModelID + } + if err := job.Repo.UpdateFATEJobInfoByUUID(job); err != nil { + return errors.Wrap(err, "failed to update FATE job info") + } + if err := job.UpdateStatus(JobStatusRunning); err != nil { + return err + } + go job.waitForJobFinish(finishCB) + return nil +} + +// waitForJobFinish checks the job status until it is finished and calls the callback function +func (job *Job) waitForJobFinish(finishCB func()) { + for job.Status == JobStatusRunning { + if err := job.CheckFATEJobStatus(); err != nil { + // TODO: exit after maximum number of retries + log.Err(err).Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Msg("failed to query job status") + } + log.Info().Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Msg("job not finished, waiting") + time.Sleep(20 * time.Second) + } + log.Info().Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Msgf("job finished, call-back exists: %v", finishCB != nil) + if finishCB != nil { + finishCB() + } +} + +// CheckFATEJobStatus issues job status query +func (job *Job) CheckFATEJobStatus() error { + if job.Status != JobStatusRunning { + return nil + } + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + + var err error + status := job.FATEJobStatus + if status, err = fateClient.QueryJobStatus(job.FATEJobID); err != nil { + log.Err(err).Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Msg("failed to query job status") + return err + } else { + log.Info().Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Str("status", status).Send() + if job.FATEJobStatus != status { + job.FATEJobStatus = status + if err := job.Repo.UpdateFATEJobStatusByUUID(job); err != nil { + return errors.Wrap(err, "failed to update FATE job status") + } + } + switch status { + case "success": + // for training job, the initiating site needs to deploy the model + // other sites can only update its model info via the updates from the initiating site + if job.Type == JobTypeTraining { + if err := job.UpdateStatus(JobStatusDeploying); err != nil { + return err + } + if job.IsInitiatingSite { + log.Info().Str("job uuid", job.UUID).Msgf("start deploying trained model") + if err := job.deployTrainedModel(); err != nil { + return errors.Wrap(err, "failed to deploy trained model") + } + if err := job.UpdateStatus(JobStatusSucceeded); err != nil { + return err + } + } else { + log.Info().Str("job uuid", job.UUID).Msgf("FATE job finished, waiting for deployed model info") + } + } else { + if err := job.UpdateStatus(JobStatusSucceeded); err != nil { + return err + } + } + case "canceled", "timeout", "failed": + if err := job.UpdateStatus(JobStatusFailed); err != nil { + return err + } + if err := job.UpdateStatusMessage("FATE Job status is: " + status); err != nil { + return err + } + case "running", "waiting": + if err := job.UpdateStatus(JobStatusRunning); err != nil { + return err + } + default: + log.Error().Msgf("unknown job status: %s", status) + } + if job.Status != JobStatusRunning { + job.FinishedAt = time.Now() + if err := job.Repo.UpdateFinishTimeByUUID(job); err != nil { + log.Err(err).Str("job uuid", job.UUID).Str("fate job id", job.FATEJobID).Msg("failed to update job finish time") + } + } + } + return nil +} + +// Update updates the job info, including the fate job status. If the job starts running, a monitoring routine is started +func (job *Job) Update(newStatus *Job) error { + if job.IsInitiatingSite { + return errors.New("current site is job initiating site") + } + if job.FATEJobID == "" || job.FATEJobStatus != newStatus.FATEJobStatus || + job.FATEModelID != newStatus.FATEModelID || job.FATEModelVersion != newStatus.FATEModelVersion { + job.FATEJobID = newStatus.FATEJobID + job.FATEJobStatus = newStatus.FATEJobStatus + job.FATEModelID = newStatus.FATEModelID + job.FATEModelVersion = newStatus.FATEModelVersion + if err := job.Repo.UpdateFATEJobInfoByUUID(job); err != nil { + return errors.Wrap(err, "failed to update FATE job info") + } + } + if job.Status != newStatus.Status { + if err := job.UpdateStatus(newStatus.Status); err != nil { + return err + } + if job.Status == JobStatusRunning { + log.Info().Msgf("job is started by the initiating site, waiting for it to finish...") + go job.waitForJobFinish(nil) + } + } + if job.StatusMessage != newStatus.StatusMessage { + if err := job.UpdateStatusMessage(newStatus.StatusMessage); err != nil { + return err + } + } + return nil +} + +// UpdateStatus updates the job's status +func (job *Job) UpdateStatus(status JobStatus) error { + // do not modify deleted jobs + if job.Status != status && job.Status != JobStatusDeleted { + job.Status = status + if err := job.Repo.UpdateStatusByUUID(job); err != nil { + return errors.Wrap(err, "failed to update job status") + } + } + return nil +} + +// UpdateStatusMessage updates the job's status message +func (job *Job) UpdateStatusMessage(message string) error { + job.StatusMessage = message + if err := job.Repo.UpdateStatusMessageByUUID(job); err != nil { + return errors.Wrap(err, "failed to update job status message") + } + return nil +} + +// UpdateResultInfo gets the job result from FATE and updates it into the repo +func (job *Job) UpdateResultInfo(partyID uint, role JobParticipantRole) error { + if job.Type == JobTypeTraining { + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + + metric, err := fateClient.GetComponentMetric(fateclient.ComponentTrackingCommonRequest{ + JobID: job.FATEJobID, + PartyID: partyID, + Role: string(role), + ComponentName: job.EvaluateComponentName, + }) + if err != nil { + return errors.Wrapf(err, "failed to query metric data") + } + resultByte, err := json.Marshal(map[string]interface{}{ + jobResultModelEvaluation: metric, + }) + if err != nil { + return err + } + job.ResultJson = string(resultByte) + } else if job.Type == JobTypePredict { + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + data, meta, err := fateClient.GetComponentOutputDataSummary(fateclient.ComponentTrackingCommonRequest{ + JobID: job.FATEJobID, + PartyID: partyID, + Role: string(role), + ComponentName: job.AlgorithmComponentName, + }) + if err != nil { + return errors.Wrapf(err, "failed to query output data") + } + resultByte, err := json.Marshal(map[string]interface{}{ + jobResultOutputData: data, + jobResultOutputDataMeta: meta, + }) + if err != nil { + return err + } + job.ResultJson = string(resultByte) + } else if job.Type == JobTypePSI { + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + componentTrackingCommonRequest := fateclient.ComponentTrackingCommonRequest{ + JobID: job.FATEJobID, + PartyID: partyID, + Role: string(role), + ComponentName: "intersection_0", + } + intersectionSummary, err := fateClient.GetComponentSummary(componentTrackingCommonRequest) + dataSummary, meta, err := fateClient.GetComponentOutputDataSummary(componentTrackingCommonRequest) + if err != nil { + return errors.Wrapf(err, "failed to query output data") + } + resultByte, err := json.Marshal(map[string]interface{}{ + jobResultOutputData: dataSummary, + jobResultOutputDataMeta: meta, + jobResultComponentSummary: intersectionSummary, + }) + if err != nil { + return err + } + job.ResultJson = string(resultByte) + } else { + return errors.Errorf("unknown job type: %v", job.Type) + } + if err := job.Repo.UpdateResultInfoByUUID(job); err != nil { + return err + } + if job.Type == JobTypeTraining { + eventExchange := event.NewSelfHttpExchange() + return eventExchange.PostEvent(event.ModelCreationEvent{ + Name: job.ModelName, + ModelID: job.FATEModelID, + ModelVersion: job.FATEModelVersion, + ComponentName: job.AlgorithmComponentName, + ProjectUUID: job.ProjectUUID, + JobUUID: job.UUID, + JobName: job.Name, + Role: string(role), + PartyID: partyID, + Evaluation: job.GetTrainingResultSummary(), + ComponentAlgorithmType: uint8(job.AlgorithmType), + }) + } + return nil +} + +// GetTrainingResultSummary returns the summary mapping of the training result. It is the evaluation info of the model +func (job *Job) GetTrainingResultSummary() map[string]string { + if job.Status != JobStatusSucceeded || job.Type != JobTypeTraining || job.ResultJson == "" { + return nil + } + EvaluationSummary := map[string]string{} + type EvaluationMeta struct { + MetricType string `json:"metric_type"` + Name string `json:"name"` + } + type EvaluationItem struct { + Data [][]interface{} `json:"data"` + Meta EvaluationMeta `json:"meta"` + } + type EvaluationInfo struct { + Train map[string]EvaluationItem `json:"train"` + Validation map[string]EvaluationItem `json:"validate"` + } + type EvaluationData struct { + ModelEvaluation EvaluationInfo `json:"model_evaluation"` + } + evaluationData := EvaluationData{} + if err := json.Unmarshal([]byte(job.ResultJson), &evaluationData); err != nil { + log.Err(err).Msg("failed to unmarshal job result") + return nil + } + buildSummary := func(itemMap map[string]EvaluationItem) { + for _, item := range itemMap { + if item.Meta.MetricType == "EVALUATION_SUMMARY" { + for _, record := range item.Data { + if len(record) == 2 { + EvaluationSummary[fmt.Sprintf("%v", record[0])] = fmt.Sprintf("%v", record[1]) + } + } + } + } + } + if len(evaluationData.ModelEvaluation.Validation) > 0 { + buildSummary(evaluationData.ModelEvaluation.Validation) + } else { + buildSummary(evaluationData.ModelEvaluation.Train) + } + return EvaluationSummary +} + +// GetPredictingResultPreview returns a fraction of the predicting result data +func (job *Job) GetPredictingResultPreview() ([]string, [][]interface{}, int) { + if job.Status != JobStatusSucceeded || job.Type != JobTypePredict || job.ResultJson == "" { + return nil, nil, -1 + } + type OutputMeta struct { + Header [][]string `json:"header"` + Total []int `json:"total"` + } + type PredictResultInfo struct { + OutputData [][][]interface{} `json:"output_data"` + OutputMeta OutputMeta `json:"output_meta"` + } + predictResultInfo := PredictResultInfo{} + if err := json.Unmarshal([]byte(job.ResultJson), &predictResultInfo); err != nil { + log.Err(err).Msg("failed to unmarshal job result") + return nil, nil, -1 + } + return predictResultInfo.OutputMeta.Header[0], predictResultInfo.OutputData[0], predictResultInfo.OutputMeta.Total[0] +} + +// GetIntersectionResult parses the PSI job result and returns the preview data +func (job *Job) GetIntersectionResult() ([]string, [][]interface{}, int, float64) { + if job.Status != JobStatusSucceeded || job.Type != JobTypePSI || job.ResultJson == "" { + return nil, nil, -1, 0 + } + type OutputMeta struct { + Header [][]string `json:"header"` + Total []int `json:"total"` + } + type ComponentSummary struct { + IntersectNum int `json:"intersect_num"` + IntersectRate float64 `json:"intersect_rate"` + } + type IntersectionResultInfo struct { + OutputData [][][]interface{} `json:"output_data"` + OutputMeta OutputMeta `json:"output_meta"` + ComponentSummary ComponentSummary `json:"component_summary"` + } + intersectionResultInfo := IntersectionResultInfo{} + if err := json.Unmarshal([]byte(job.ResultJson), &intersectionResultInfo); err != nil { + log.Err(err).Msg("failed to unmarshal intersection result") + return nil, nil, -1, 0 + } + res := intersectionResultInfo + if res.ComponentSummary.IntersectNum == 0 || res.ComponentSummary.IntersectRate == 0 { + return nil, nil, 0, 0 + } + return res.OutputMeta.Header[0], res.OutputData[0], res.ComponentSummary.IntersectNum, res.ComponentSummary.IntersectRate +} + +// deployTrainedModel deploys the trained model and update job with the newly deployed model's info +func (job *Job) deployTrainedModel() error { + if job.Type != JobTypeTraining { + return nil + } + if job.FATEJobStatus != "success" { + log.Warn().Str("job uuid", job.UUID).Msgf("fate job status is %s, cannot deploy model", job.FATEJobStatus) + return nil + } + fateClient := fateclient.NewFATEFlowClient(job.FATEFlowContext.FATEFlowHost, job.FATEFlowContext.FATEFlowPort, job.FATEFlowContext.FATEFlowIsHttps) + componentList := job.AlgorithmConfig.TrainingComponentsToDeploy + if len(componentList) == 0 { + log.Error().Msg("there should have been at least one component in the to-deploy list") + } + modelInfo, err := fateClient.DeployModel(fateclient.ModelDeployRequest{ + ModelInfo: fateclient.ModelInfo{ + ModelID: job.FATEModelID, + ModelVersion: job.FATEModelVersion, + }, + ComponentList: componentList, + }) + if err != nil { + return errors.Wrap(err, "failed to ask FATE to deploy model") + } + job.FATEModelID = modelInfo.ModelID + job.FATEModelVersion = modelInfo.ModelVersion + return job.Repo.UpdateFATEJobInfoByUUID(job) +} diff --git a/site-portal/server/domain/entity/job_participant.go b/site-portal/server/domain/entity/job_participant.go new file mode 100644 index 00000000..9e670ac7 --- /dev/null +++ b/site-portal/server/domain/entity/job_participant.go @@ -0,0 +1,85 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// JobParticipant represents a site and its data for a job +type JobParticipant struct { + gorm.Model + UUID string `gorm:"type:varchar(36)"` + JobUUID string `gorm:"type:varchar(36)"` + SiteUUID string `gorm:"type:varchar(36)"` + SiteName string `gorm:"type:varchar(255)"` + SitePartyID uint + SiteRole JobParticipantRole `gorm:"type:varchar(255)"` + DataUUID string `gorm:"type:varchar(36)"` + DataName string `gorm:"type:varchar(255)"` + DataDescription string `gorm:"type:text"` + DataTableName string `gorm:"type:varchar(255)"` + DataTableNamespace string `gorm:"type:varchar(255)"` + DataLabelName string `gorm:"type:varchar(255)"` + Status JobParticipantStatus + Repo repo.JobParticipantRepository `gorm:"-"` +} + +// JobParticipantStatus is the status of this participant in the job +type JobParticipantStatus uint8 + +const ( + JobParticipantStatusUnknown JobParticipantStatus = iota + JobParticipantStatusInitiator + JobParticipantStatusPending + JobParticipantStatusApproved + JobParticipantStatusRejected +) + +func (s JobParticipantStatus) String() string { + names := map[JobParticipantStatus]string{ + JobParticipantStatusUnknown: "Unknown", + JobParticipantStatusPending: "Pending", + JobParticipantStatusRejected: "Rejected", + JobParticipantStatusApproved: "Approved", + JobParticipantStatusInitiator: "Auto-approved as Initiator", + } + return names[s] +} + +// JobParticipantRole is the enum of roles of a participant +type JobParticipantRole string + +const ( + JobParticipantRoleGuest JobParticipantRole = "guest" + JobParticipantRoleHost JobParticipantRole = "host" +) + +// Create initialize the participant info and create it in the repo +func (p *JobParticipant) Create() error { + p.UUID = uuid.NewV4().String() + if err := p.Repo.Create(p); err != nil { + return err + } + return nil +} + +// UpdateStatus changes the participant's status +func (p *JobParticipant) UpdateStatus(status JobParticipantStatus) error { + p.Status = status + return p.Repo.UpdateStatusByUUID(p) +} diff --git a/site-portal/server/domain/entity/local_data.go b/site-portal/server/domain/entity/local_data.go new file mode 100644 index 00000000..38755a10 --- /dev/null +++ b/site-portal/server/domain/entity/local_data.go @@ -0,0 +1,377 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "bytes" + "database/sql/driver" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +var ( + once sync.Once + baseDir string +) + +// LocalData represents an uploaded data file +type LocalData struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + // Name is the name to reference the data + Name string `gorm:"type:varchar(255);not null"` + // Description contains more text about the data + Description string `json:"description" gorm:"type:text"` + // Column is a list of headers in this data + Column Headers `json:"column" gorm:"type:text"` + // TableName is the name of the data in FATE system + TableName string `json:"table_name" gorm:"type:varchar(255)"` + // TableNamespace is the namespace of the data in FATE system + TableNamespace string `json:"table_namespace" gorm:"type:varchar(255)"` + // Count is the number of the samples in the data + Count uint64 `json:"count"` + // Features is feature name list + Features Headers `json:"feature_size" gorm:"type:text"` + // Preview is the first 10 lines of data in this data + Preview string `json:"preview" gorm:"type:text"` + // IDMetaInfo is the meta data describing the ID column + IDMetaInfo *valueobject.IDMetaInfo `json:"id_meta_info" gorm:"type:text;column:id_meta_info"` + // JobID is the related FATE upload job id + JobID string `json:"-" gorm:"type:varchar(255);column:job_id"` + // JobConf is the related FATE upload job conf + JobConf string `json:"-" gorm:"type:text;column:job_conf"` + // JobStatus is the current status of the data in FATE system + JobStatus UploadJobStatus `json:"status"` + // JobErrorString records the error message if the job failed + JobErrorMsg string `json:"job_error_msg" gorm:"type:text"` + // LocalFilePath is the file path relative to the baseDir + LocalFilePath string `json:"-" gorm:"type:varchar(255)"` + // UploadContext contains info needed to finish the upload job + UploadContext UploadContext `json:"-" gorm:"-"` + // Repo is used to store the necessary data into the storage + Repo repo.LocalDataRepository `json:"-" gorm:"-"` +} + +// UploadContext currently contains FATE flow connection info +type UploadContext struct { + // FATEFlowHost is the host address of the service + FATEFlowHost string + // FATEFlowPort is the port of the service + FATEFlowPort uint + // FATEFlowIsHttps is whether the connection should be over TLS + FATEFlowIsHttps bool +} + +// UploadJobStatus is the status of the data in FATE system +type UploadJobStatus uint8 + +const ( + UploadJobStatusToBeCreated UploadJobStatus = iota + UploadJobStatusCreating + UploadJobStatusRunning + UploadJobStatusFailed + UploadJobStatusSucceeded +) + +// MarshalJSON convert Cluster status to string +func (s *UploadJobStatus) MarshalJSON() ([]byte, error) { + names := map[UploadJobStatus]string{ + UploadJobStatusToBeCreated: `"ToBeCreated"`, + UploadJobStatusCreating: `"Creating"`, + UploadJobStatusRunning: `"Running"`, + UploadJobStatusFailed: `"Failed"`, + UploadJobStatusSucceeded: `"Succeeded"`, + } + return bytes.NewBufferString(names[*s]).Bytes(), nil +} + +// Headers are local data column names +type Headers []string + +func (h Headers) Value() (driver.Value, error) { + bJson, err := json.Marshal(h) + return bJson, err +} + +func (h *Headers) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), h) +} + +// Upload save the file into local storage and uploaded it to FATE system +func (d *LocalData) Upload(fileHeader *multipart.FileHeader) error { + if d.UploadContext.FATEFlowHost == "" || d.UploadContext.FATEFlowPort == 0 { + return errors.Errorf("cannot find valid FATE flow connection info") + } + if err := d.Repo.CheckNameConflict(d.Name); err != nil { + return err + } + src, err := fileHeader.Open() + if err != nil { + return err + } + defer src.Close() + d.UUID = uuid.NewV4().String() + parentDir := filepath.Join(getBaseDir(), d.UUID) + if err = os.MkdirAll(parentDir, 0700); err != nil { + return err + } + // check if the file name is valid + if strings.TrimSpace(fileHeader.Filename) == "" { + return errors.Errorf("file name can not be empty") + } + filename := strings.Split(fileHeader.Filename, ".")[0] + var isStringAlphabetic = regexp.MustCompile(`^[a-zA-Z0-9\s\d\-_/]*$`).MatchString + if !isStringAlphabetic(filename) { + return errors.Errorf("file name can not contain special characters") + } + if len(filename) < 2 { + return errors.Errorf("file name is too short") + } + if len(filename) > 255 { + return errors.Errorf("file name is too long") + } + // parse the csv file and record some meta data and previews + if csvErr := func() error { + csvFile, err := fileHeader.Open() + if err != nil { + return err + } + defer csvFile.Close() + csvReader := csv.NewReader(csvFile) + + // check headers + headers, err := csvReader.Read() + if err != nil { + return nil + } + d.Column = headers + if features, containsID := func() ([]string, bool) { + containsID := false + features := Headers{} + for _, header := range headers { + if strings.ToLower(header) == "id" { + containsID = true + } else if strings.ToLower(header) != "y" { + features = append(features, header) + } + } + return features, containsID + }(); containsID == false { + return errors.New("data must contain an id field") + } else { + d.Features = features + } + + // count data lines and keep the first 10 lines as preview + if count, previewJsonStr, err := func() (uint64, string, error) { + var records []map[string]string + var lines uint64 + for { + recordLine, err := csvReader.Read() + if err != nil { + break + } + if lines < 10 { + record := map[string]string{} + for i, value := range recordLine { + record[headers[i]] = value + } + records = append(records, record) + } + lines++ + } + if err != nil && err != io.EOF { + return 0, "", err + } + jsonBytes, err := json.Marshal(records) + if err != nil { + return 0, "", err + } + return lines, string(jsonBytes), nil + }(); err != nil { + return errors.Wrap(err, "error parsing data records") + } else { + d.Count = count + d.Preview = previewJsonStr + } + return nil + }(); csvErr != nil { + return errors.Wrap(csvErr, "error parsing the uploaded csv file") + } + + d.LocalFilePath = filepath.Join(d.UUID, filepath.Base(fileHeader.Filename)) + dst, err := os.Create(filepath.Join(getBaseDir(), d.LocalFilePath)) + if err != nil { + return err + } + log.Info().Msgf("saving uploaded data file %s to %s", fileHeader.Filename, dst.Name()) + _, err = io.Copy(dst, src) + if err != nil { + return err + } + d.JobStatus = UploadJobStatusToBeCreated + d.IDMetaInfo = nil + d.TableName = fmt.Sprintf("table-%s", d.UUID) + d.TableNamespace = fmt.Sprintf("ns-%s", d.UUID) + if err := d.Repo.Create(d); err != nil { + return err + } + + go func() { + log.Info().Msgf("uploading data file %s to FATE", dst.Name()) + fateClient := fateclient.NewFATEFlowClient(d.UploadContext.FATEFlowHost, d.UploadContext.FATEFlowPort, d.UploadContext.FATEFlowIsHttps) + d.ChangeJobStatus(UploadJobStatusCreating) + // 2 stands for SPARK_PULSAR + backend := 2 + if viper.GetBool("siteportal.fate.eggroll.enabled") { + backend = 0 + } + uploadConf := fateclient.DataUploadRequest{ + File: dst.Name(), + Head: 1, + Partition: 8, // XXX: use viper configuration instead of a hard-code one + WorkMode: 1, + Backend: backend, + Namespace: d.TableNamespace, + TableName: d.TableName, + Drop: 1, + } + confBytes, _ := json.Marshal(uploadConf) + d.JobConf = string(confBytes) + if jobID, err := fateClient.UploadData(uploadConf); err != nil { + log.Err(err).Msgf("failed to upload data %s to FATE", d.UUID) + d.ChangeJobStatus(UploadJobStatusFailed) + d.JobErrorMsg = err.Error() + } else { + log.Info().Msgf("uploading job ID is %s", jobID) + d.ChangeJobStatus(UploadJobStatusRunning) + d.JobID = jobID + } + if err := d.Repo.UpdateJobInfoByUUID(d); err != nil { + log.Err(err).Str("data uuid", d.UUID).Msg("failed to update data job info") + return + } + // TODO: we should use a cron-like task to monitor the job status, preferably even be able to survive service restarts + func() { + for d.JobStatus == UploadJobStatusRunning { + if status, err := fateClient.QueryJobStatus(d.JobID); err != nil { + // TODO: exit after maximum number of retries and set job status to failed + log.Err(err).Str("data uuid", d.UUID).Msg("failed to query job status") + } else if status == "success" { + d.ChangeJobStatus(UploadJobStatusSucceeded) + } else if status == "canceled" || status == "timeout" || status == "failed" { + log.Error().Str("data uuid", d.UUID).Str("status", status).Send() + d.ChangeJobStatus(UploadJobStatusFailed) + return + } else { + log.Info().Str("data uuid", d.UUID).Str("status", status).Send() + } + time.Sleep(5 * time.Second) + } + }() + log.Info().Str("data uuid", d.UUID).Str("job id", d.JobID). + Msgf("finished monitoring uploading job") + }() + return nil +} + +// ChangeJobStatus upload the data's upload job status +func (d *LocalData) ChangeJobStatus(newStatus UploadJobStatus) { + d.JobStatus = newStatus + if err := d.Repo.UpdateJobInfoByUUID(d); err != nil { + log.Err(err).Str("data uuid", d.UUID).Msgf("failed to update data status") + } +} + +// GetAbsFilePath returns the absolute path the local date file +func (d *LocalData) GetAbsFilePath() (string, error) { + absPath := filepath.Join(getBaseDir(), d.LocalFilePath) + if _, err := os.Stat(absPath); err == nil { + return absPath, nil + } else if os.IsNotExist(err) { + return "", errors.Errorf("file no longer exists") + } else { + return "", errors.Wrap(err, "error checking file stat") + } +} + +func (d *LocalData) Destroy() error { + log.Info().Str("data uuid", d.UUID).Msgf("removing data") + // remove the data file + absPath := filepath.Join(getBaseDir(), d.LocalFilePath) + if err := func() error { + if _, err := os.Stat(absPath); err != nil { + if os.IsNotExist(err) { + return nil + } else { + return errors.Wrap(err, "error checking file stat") + } + } + if err := os.RemoveAll(filepath.Dir(absPath)); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrapf(err, "error deleting file") + } + log.Info().Msgf("deleting table %s namespace: %s from FATE", d.TableName, d.TableNamespace) + fateClient := fateclient.NewFATEFlowClient(d.UploadContext.FATEFlowHost, d.UploadContext.FATEFlowPort, d.UploadContext.FATEFlowIsHttps) + if err := fateClient.DeleteTable(d.TableNamespace, d.TableName); err != nil { + // TODO: retry + log.Err(err).Msgf("error deleting table from FATE") + } + // delete the record from repo + return d.Repo.DeleteByUUID(d.UUID) +} + +// UpdateIDMetaInfo changes the meta info of the id column +func (d *LocalData) UpdateIDMetaInfo(info *valueobject.IDMetaInfo) error { + d.IDMetaInfo = info + return d.Repo.UpdateIDMetaInfoByUUID(d.UUID, d.IDMetaInfo) +} + +func getBaseDir() string { + once.Do(func() { + baseDir = viper.GetString("siteportal.localdata.basedir") + if baseDir == "" { + panic("empty base folder") + } + if !filepath.IsAbs(baseDir) { + panic("baseFolder should be absolute path") + } + if err := os.MkdirAll(baseDir, 0700); err != nil { + panic(err) + } + }) + return baseDir +} diff --git a/site-portal/server/domain/entity/model.go b/site-portal/server/domain/entity/model.go new file mode 100644 index 00000000..26e5ac22 --- /dev/null +++ b/site-portal/server/domain/entity/model.go @@ -0,0 +1,60 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// Model is the domain entity of the model management context +type Model struct { + gorm.Model + UUID string `gorm:"type:varchar(36)"` + Name string `gorm:"type:varchar(255)"` + FATEModelID string `gorm:"type:varchar(255);column:fate_model_id"` + FATEModelVersion string `gorm:"type:varchar(255);column:fate_model_version"` + ProjectUUID string `gorm:"type:varchar(36)"` + ProjectName string `gorm:"type:varchar(255)"` + JobUUID string `gorm:"type:varchar(36)"` + JobName string `gorm:"type:varchar(255)"` + ComponentName string `gorm:"type:varchar(255)"` + ComponentAlgorithmType ComponentAlgorithmType + Role string `gorm:"type:varchar(255)"` + PartyID uint + Evaluation valueobject.ModelEvaluation `gorm:"type:text"` + Repo repo.ModelRepository `gorm:"-"` +} + +//ComponentAlgorithmType is the type enum of the algorithm +type ComponentAlgorithmType uint8 + +const ( + ComponentAlgorithmTypeUnknown ComponentAlgorithmType = iota + ComponentAlgorithmTypeHomoLR + ComponentAlgorithmTypeHomoSBT +) + +// Create initializes the model and creates it in the repo +func (model *Model) Create() error { + model.UUID = uuid.NewV4().String() + if err := model.Repo.Create(model); err != nil { + return errors.Wrap(err, "failed to create model") + } + return nil +} diff --git a/site-portal/server/domain/entity/model_deployment.go b/site-portal/server/domain/entity/model_deployment.go new file mode 100644 index 00000000..27d865c8 --- /dev/null +++ b/site-portal/server/domain/entity/model_deployment.go @@ -0,0 +1,137 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "encoding/json" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient" + "github.com/hashicorp/go-version" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// ModelDeployment represents a deployment operation for a model +type ModelDeployment struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36)"` + ServiceName string `json:"service_name" gorm:"type:varchar(255)"` + ModelUUID string `json:"model_uuid" gorm:"type:varchar(36)"` + Type ModelDeploymentType `json:"type"` + Status ModelDeploymentStatus `json:"status"` + DeploymentParametersJson string `json:"parameters_json" gorm:"type:text"` + RequestJson string `json:"request_json" gorm:"type:text"` + ResultJson string `json:"result_json" gorm:"type:text"` + Repo repo.ModelDeploymentRepository `json:"-" gorm:"-"` +} + +// ModelDeploymentType is a enum of the types of the target deployment runtime +// We use uint instead of uint8 here because json marshalling will convert []uint8 slice to base64-encoded string +type ModelDeploymentType uint + +const ( + ModelDeploymentTypeUnknown ModelDeploymentType = iota + ModelDeploymentTypeKFServing +) + +func (t ModelDeploymentType) String() string { + switch t { + case ModelDeploymentTypeKFServing: + return "kfserving" + default: + return "unknown" + } +} + +// ModelDeploymentStatus is a enum of the status of the deployment action +type ModelDeploymentStatus uint8 + +const ( + ModelDeploymentStatusUnknown ModelDeploymentStatus = iota + ModelDeploymentStatusCreated + ModelDeploymentStatusFailed + ModelDeploymentStatusSucceeded +) + +// ModelDeploymentContext contains the context needed to perform a deployment action +type ModelDeploymentContext struct { + Model *Model + FATEFlowContext FATEFlowContext + KubeflowConfig valueobject.KubeflowConfig + UserParametersJson string +} + +var minHomoDeploymentFATEVersion = func() *version.Version { + version, _ := version.NewVersion("1.7.0") + return version +}() + +// Deploy deploys the model to FATE +func (d *ModelDeployment) Deploy(context ModelDeploymentContext) error { + fateClient := fateclient.NewFATEFlowClient(context.FATEFlowContext.FATEFlowHost, context.FATEFlowContext.FATEFlowPort, context.FATEFlowContext.FATEFlowIsHttps) + versionStr, err := fateClient.GetFATEVersion() + if err != nil { + return err + } + currentVersion, err := version.NewVersion(versionStr) + if err != nil { + return err + } + if currentVersion.LessThan(minHomoDeploymentFATEVersion) { + return errors.Errorf("current FATE version (%s) is lower than the supportted version (%s)", currentVersion.String(), minHomoDeploymentFATEVersion.String()) + } + + basicModelInfo := fateclient.HomoModelConversionRequest{ + ModelInfo: fateclient.ModelInfo{ + ModelID: context.Model.FATEModelID, + ModelVersion: context.Model.FATEModelVersion, + }, + PartyID: context.Model.PartyID, + Role: context.Model.Role, + } + if err := fateClient.ConvertHomoModel(basicModelInfo); err != nil { + return errors.Wrapf(err, "failed to convert model") + } + + var parameterInstance map[string]interface{} + if err := json.Unmarshal([]byte(d.DeploymentParametersJson), ¶meterInstance); err != nil { + return err + } + d.ResultJson, err = fateClient.DeployHomoModel(fateclient.HomoModelDeploymentRequest{ + HomoModelConversionRequest: basicModelInfo, + ServiceID: d.ServiceName, + ComponentName: context.Model.ComponentName, + DeploymentType: d.Type.String(), + DeploymentParameters: parameterInstance, + }) + if err != nil { + d.Status = ModelDeploymentStatusFailed + } else { + d.Status = ModelDeploymentStatusSucceeded + } + if err := d.Repo.UpdateStatusByUUID(d); err != nil { + log.Err(err).Msg("failed to update deployment status") + } + if err := d.Repo.UpdateResultJsonByUUID(d); err != nil { + log.Err(err).Msg("failed to update deployment result json") + } + if err != nil { + return errors.Wrapf(err, "failed to deploy model") + } + return nil +} diff --git a/site-portal/server/domain/entity/project.go b/site-portal/server/domain/entity/project.go new file mode 100644 index 00000000..539d2618 --- /dev/null +++ b/site-portal/server/domain/entity/project.go @@ -0,0 +1,94 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "gorm.io/gorm" +) + +// Project is a container for federated machine learning jobs +type Project struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + // Name is the name of the project + Name string `gorm:"type:varchar(255);not null"` + // Description contains more text about the project + Description string `json:"description" gorm:"type:text"` + // AutoApprovalEnabled is whether new jobs will be automatically approved + AutoApprovalEnabled bool `json:"auto_approval_enabled"` + // Type is the project type + Type ProjectType + // Status is the status of the project + Status ProjectStatus + // Creating/Managing site info + valueobject.ProjectCreatorInfo + // The repo for persistence + Repo repo.ProjectRepository `json:"-" gorm:"-"` +} + +// ProjectType is the project type +type ProjectType uint8 + +const ( + ProjectTypeLocal ProjectType = iota + 1 + // ProjectTypeFederatedLocal means the project is locally created and is tracked in the FML manager + ProjectTypeFederatedLocal + ProjectTypeRemote +) + +// ProjectStatus is the status of a project +type ProjectStatus uint8 + +const ( + ProjectStatusManaged ProjectStatus = iota + 1 + ProjectStatusPending + ProjectStatusJoined + ProjectStatusRejected + ProjectStatusLeft + ProjectStatusClosed + ProjectStatusDismissed +) + +// Create creates the project +func (p *Project) Create() error { + if p.Name == "" { + return errors.New("empty project name") + } + if p.ProjectCreatorInfo.ManagingSiteName == "" || p.ProjectCreatorInfo.ManagingSitePartyID == 0 { + return errors.New("site info not configured") + } + switch p.Type { + case ProjectTypeLocal: + p.UUID = uuid.NewV4().String() + if err := p.Repo.CheckNameConflict(p.Name); err != nil { + return err + } + p.Status = ProjectStatusManaged + case ProjectTypeRemote: + if p.UUID == "" { + return errors.New("missing project uuid") + } + p.Status = ProjectStatusPending + default: + return errors.Errorf("invalid project type: %d", p.Type) + } + log.Info().Str("name", p.Name).Interface("type", p.Type).Msg("create project") + return p.Repo.Create(p) +} diff --git a/site-portal/server/domain/entity/project_data.go b/site-portal/server/domain/entity/project_data.go new file mode 100644 index 00000000..a4bc06ad --- /dev/null +++ b/site-portal/server/domain/entity/project_data.go @@ -0,0 +1,60 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "gorm.io/gorm" +) + +// ProjectData represents the data association in a project +type ProjectData struct { + gorm.Model + Name string `json:"name" gorm:"type:varchar(255)"` + Description string `json:"description" gorm:"type:text"` + UUID string `json:"uuid" gorm:"type:varchar(36)"` + ProjectUUID string `json:"project_uuid" gorm:"type:varchar(36)"` + DataUUID string `json:"data_uuid" gorm:"type:varchar(36)"` + SiteUUID string `json:"site_uuid" gorm:"type:varchar(36)"` + SiteName string `json:"site_name" gorm:"type:varchar(255)"` + SitePartyID uint `json:"site_party_id"` + Type ProjectDataType `json:"type"` + Status ProjectDataStatus `json:"status"` + TableName string `json:"table_name" gorm:"type:varchar(255)"` + TableNamespace string `json:"table_namespace" gorm:"type:varchar(255)"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` + Repo repo.ProjectDataRepository `json:"-" gorm:"-"` +} + +// ProjectDataType is the type of this association +type ProjectDataType uint8 + +const ( + ProjectDataTypeUnknown ProjectDataType = iota + ProjectDataTypeLocal + ProjectDataTypeRemote +) + +// ProjectDataStatus is the status of this association +type ProjectDataStatus uint8 + +const ( + ProjectDataStatusUnknown ProjectDataStatus = iota + ProjectDataStatusDismissed + ProjectDataStatusAssociated +) diff --git a/site-portal/server/domain/entity/project_invitation.go b/site-portal/server/domain/entity/project_invitation.go new file mode 100644 index 00000000..42ccaea6 --- /dev/null +++ b/site-portal/server/domain/entity/project_invitation.go @@ -0,0 +1,38 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// ProjectInvitation is the invitation for a project +type ProjectInvitation struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + ProjectUUID string `gorm:"type:varchar(36)"` + SiteUUID string `gorm:"type:varchar(36)"` + Status ProjectInvitationStatus +} + +// ProjectInvitationStatus is the status of the invitation +type ProjectInvitationStatus uint8 + +const ( + // ProjectInvitationStatusCreated means the invitation is created but hasn't been sent yet + ProjectInvitationStatusCreated ProjectInvitationStatus = iota + ProjectInvitationStatusSent + ProjectInvitationStatusRevoked + ProjectInvitationStatusAccepted + ProjectInvitationStatusRejected +) diff --git a/site-portal/server/domain/entity/project_participant.go b/site-portal/server/domain/entity/project_participant.go new file mode 100644 index 00000000..1d010e30 --- /dev/null +++ b/site-portal/server/domain/entity/project_participant.go @@ -0,0 +1,43 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import "gorm.io/gorm" + +// ProjectParticipant is a site in a project +type ProjectParticipant struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36);index;unique"` + ProjectUUID string `json:"project_uuid" gorm:"type:varchar(36)"` + SiteUUID string `json:"site_uuid" gorm:"type:varchar(36)"` + SiteName string `json:"site_name" gorm:"type:varchar(255)"` + SitePartyID uint `json:"site_party_id"` + SiteDescription string `json:"site_description"` + Status ProjectParticipantStatus `json:"status"` +} + +// ProjectParticipantStatus is the status of the site in a project +type ProjectParticipantStatus uint8 + +const ( + ProjectParticipantStatusUnknown ProjectParticipantStatus = iota + ProjectParticipantStatusOwner + ProjectParticipantStatusPending + ProjectParticipantStatusJoined + ProjectParticipantStatusRejected + ProjectParticipantStatusLeft + ProjectParticipantStatusDismissed + ProjectParticipantStatusRevoked +) diff --git a/site-portal/server/domain/entity/site.go b/site-portal/server/domain/entity/site.go new file mode 100644 index 00000000..e781f234 --- /dev/null +++ b/site-portal/server/domain/entity/site.go @@ -0,0 +1,199 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "strings" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/event" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fateclient" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fmlmanager" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +// Site contains all the info for the current site +type Site struct { + gorm.Model + UUID string `json:"uuid" gorm:"type:varchar(36);index;unique"` + // Name is the site's name + Name string `json:"name" gorm:"type:varchar(255);unique;not null"` + // Description contains more text about this site + Description string `json:"description" gorm:"type:text"` + // PartyID is the id of this party + PartyID uint `json:"party_id" gorm:"column:party_id"` + // ExternalHost is the IP or hostname this site portal service is exposed + ExternalHost string `json:"external_host" gorm:"type:varchar(255);column:external_ip"` + // ExternalPort the port number this site portal service is exposed + ExternalPort uint `json:"external_port" gorm:"column:external_port"` + // HTTPS choose if site portal has HTTPS enabled + HTTPS bool `json:"https" gorm:"column:https"` + // FMLManagerEndpoint is of format "://:" + FMLManagerEndpoint string `json:"fml_manager_endpoint" gorm:"type:varchar(255);column:fml_manager_endpoint"` + // FMLManagerServerName is used to verify FML Manager's certificate + FMLManagerServerName string `json:"fml_manager_server_name" gorm:"type:varchar(255);column:fml_manager_server_name"` + // FMLManagerConnectedAt is the last time this portal has registered to a FML manager + FMLManagerConnectedAt time.Time `json:"fml_manager_connected_at" gorm:"column:fml_manager_connected_at"` + // FMLManagerConnected is whether the portal is connected to FML manager + FMLManagerConnected bool `json:"fml_manager_connected" gorm:"column:fml_manager_connected"` + // FATEFlowHost is the host address of the FATE-flow service + FATEFlowHost string `json:"fate_flow_host" gorm:"type:varchar(255);column:fate_flow_host"` + // FATEFlowHTTPPort is the http port number of the FATE-flow service + FATEFlowHTTPPort uint `json:"fate_flow_http_port" gorm:"column:fate_flow_http_port"` + // FATEFlowGRPCPort is the grpc port number of the FATE-flow service, currently not used + FATEFlowGRPCPort uint `json:"fate_flow_grpc_port" gorm:"column:fate_flow_grpc_port"` + // FATEFlowConnectedAt is the last time this portal connected to the FATE flow + FATEFlowConnectedAt time.Time `json:"fate_flow_connected_at" gorm:"column:fate_flow_connected_at"` + // FATEFlowConnected is whether the portal has connected to FATEFlow after the address is configured + FATEFlowConnected bool `json:"fate_flow_connected" gorm:"column:fate_flow_connected"` + // KubeflowConfig records the Kubeflow related information for deploying horizontal model to the KFServing system + KubeflowConfig valueobject.KubeflowConfig `json:"kubeflow_config" gorm:"type:text;column:kubeflow_config"` + // KubeflowConnectedAt is the last time this portal has successfully connected all related Kubeflow service + KubeflowConnectedAt time.Time `json:"kubeflow_connected_at" gorm:"column:kubeflow_connected_at"` + // KubeflowConnected is whether this site has connected to the Kubeflow since it is configured + KubeflowConnected bool `json:"kubeflow_connected" gorm:"column:kubeflow_connected"` + // Repo is the repository interface + Repo repo.SiteRepository `json:"-" gorm:"-"` +} + +// Validate if the site has been properly configured +func (site *Site) Validate() error { + if site.Name == "" || site.PartyID == 0 { + return errors.New("Name or Party ID missing") + } + return nil +} + +// Load site information from repository +func (site *Site) Load() error { + return site.Repo.Load(site) +} + +// UpdateConfigurableInfo changes the site information +func (site *Site) UpdateConfigurableInfo(updatedSite *Site) error { + // load the site info first + if err := site.Load(); err != nil { + return err + } + + // set the FML manager connected flag to false if key infos are changed + if updatedSite.FMLManagerEndpoint != site.FMLManagerEndpoint || + updatedSite.Name != site.Name || + updatedSite.Description != site.Description || + updatedSite.PartyID != site.PartyID || + updatedSite.ExternalHost != site.ExternalHost || + updatedSite.ExternalPort != site.ExternalPort || + updatedSite.HTTPS != site.HTTPS || + updatedSite.FMLManagerServerName != site.FMLManagerServerName { + log.Info().Msgf("site info or FML manager info changed, marking site as unregistered") + site.FMLManagerConnected = false + } + // only writes the configurable info + site.Name = updatedSite.Name + site.Description = updatedSite.Description + site.PartyID = updatedSite.PartyID + site.ExternalHost = updatedSite.ExternalHost + site.ExternalPort = updatedSite.ExternalPort + site.FMLManagerEndpoint = updatedSite.FMLManagerEndpoint + site.FMLManagerServerName = updatedSite.FMLManagerServerName + site.FATEFlowHost = updatedSite.FATEFlowHost + site.FATEFlowHTTPPort = updatedSite.FATEFlowHTTPPort + site.FATEFlowGRPCPort = updatedSite.FATEFlowGRPCPort + site.KubeflowConfig = updatedSite.KubeflowConfig + site.HTTPS = updatedSite.HTTPS + if err := site.Repo.Update(site); err != nil { + return err + } + go func() { + if err := event.NewSelfHttpExchange().PostEvent(event.ProjectParticipantUpdateEvent{ + UUID: site.UUID, + PartyID: site.PartyID, + Name: site.Name, + Description: site.Description, + }); err != nil { + log.Err(err).Msgf("failed to post site info update event") + } + }() + return nil +} + +// ConnectFATEFlow try to issue a test request to the FATE-flow service +func (site *Site) ConnectFATEFlow(host string, port uint, https bool) error { + client := fateclient.NewFATEFlowClient(host, port, https) + return client.TestConnection() +} + +// RegisterToFMLManager registers this site to the FML manager service +func (site *Site) RegisterToFMLManager(endpoint string, serverName string) error { + // load the site info first + if err := site.Load(); err != nil { + return err + } + if site.Name == "" || site.PartyID == 0 || site.ExternalHost == "" || site.ExternalPort == 0 { + return errors.New("site info incomplete") + } + endpoint = strings.TrimSpace(endpoint) + serverName = strings.TrimSpace(serverName) + if !strings.HasPrefix(endpoint, "http") { + return errors.New("invalid endpoint: http:// or https:// schema is needed") + } + sitePortalCommonName := strings.TrimSpace(viper.GetString("siteportal.tls.common.name")) + if site.HTTPS && sitePortalCommonName == "" { + return errors.New("missing required variable 'SITEPORTAL_TLS_COMMON_NAME' when site is using HTTPs") + } + endpoint = strings.TrimSuffix(endpoint, "/") + client := fmlmanager.NewFMLManagerClient(endpoint, serverName) + log.Info().Msgf("connecting FML manager at %s", endpoint) + err := client.CreateSite(&fmlmanager.Site{ + UUID: site.UUID, + Name: site.Name, + Description: site.Description, + PartyID: site.PartyID, + ExternalHost: site.ExternalHost, + ExternalPort: site.ExternalPort, + HTTPS: site.HTTPS, + ServerName: sitePortalCommonName, + }) + if err != nil { + return errors.Wrapf(err, "failed to connect to fml-manager at %s", endpoint) + } + log.Info().Msgf("connected to fml manager at %s", endpoint) + wasConnected := site.FMLManagerConnected + site.FMLManagerConnected = true + site.FMLManagerConnectedAt = time.Now() + site.FMLManagerEndpoint = endpoint + site.FMLManagerServerName = serverName + if err := site.Repo.UpdateFMLManagerConnectionStatus(site); err != nil { + return errors.Wrapf(err, "failed to update fml connection status") + } + // sync remote projects + if wasConnected == false { + go func() { + log.Info().Msg("syncing projects list after re-connected to FML manager") + exchange := event.NewSelfHttpExchange() + if err := exchange.PostEvent(event.ProjectListSyncEvent{}); err != nil { + log.Err(err).Msg("failed to sync projects list") + return + } + log.Info().Msg("done syncing projects list after re-connected to FML manager") + }() + } + return nil +} diff --git a/site-portal/server/domain/entity/user.go b/site-portal/server/domain/entity/user.go new file mode 100644 index 00000000..f99504f3 --- /dev/null +++ b/site-portal/server/domain/entity/user.go @@ -0,0 +1,98 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +import ( + "regexp" + "strings" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// User is a representation of the user available in the site +type User struct { + gorm.Model + UUID string `gorm:"type:varchar(36);index;unique"` + // Name is the user's name + Name string `gorm:"type:varchar(255);unique;not null"` + // Password is the user's hashed password + Password string `gorm:"type:varchar(255)"` + // PermissionInfo records the user's access to the system + PermissionInfo valueobject.UserPermissionInfo `gorm:"embedded"` + // Repo is the repository to persistent related data + Repo repo.UserRepository `gorm:"-"` +} + +// UpdatePermissionInfo changes the user's permission +func (u *User) UpdatePermissionInfo(info valueobject.UserPermissionInfo) error { + if u.Name == "Admin" && info.SitePortalAccess == false { + return errors.Errorf("Cannot set Admin user's site portal access to false") + } + u.PermissionInfo = info + return u.Repo.UpdatePermissionInfoById(u.ID, info) +} + +// LoadById reads the info from the repo +func (u *User) LoadById() error { + return u.Repo.LoadById(u) +} + +// CheckSitePortalAccess returns error if the user doesn't have the portal access +func (u *User) CheckSitePortalAccess() error { + if u.PermissionInfo.SitePortalAccess == false { + return errors.Errorf("user: %s doesn't have site portal access", u.Name) + } + return nil +} + +// UpdatePwdInfo updates a user's password +func (u *User) UpdatePwdInfo(curPassword, newPassword string) error { + //Check the input of current password is matching to record + if err := func() error { + if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(curPassword)); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + // check if the new password is valid + if strings.TrimSpace(newPassword) == "" { + return errors.Errorf("new password can not be empty") + } + if curPassword == newPassword { + return errors.Errorf("new password can not be same to the current password") + } + if len(newPassword) < 8 || len(newPassword) > 20 { + return errors.Errorf("new password should be 8-20 characters long") + } + var hasUpperCase = regexp.MustCompile(`[A-Z]`).MatchString + var hasLowerCase = regexp.MustCompile(`[a-z]`).MatchString + var hasNumbers = regexp.MustCompile(`[0-9]`).MatchString + + if !hasUpperCase(newPassword) || !hasLowerCase(newPassword) || !hasNumbers(newPassword) { + return errors.Errorf("password should be with at least 1 uppercase, 1 lowercase and 1 number") + } + // hash the new password + hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + return u.Repo.UpdatePasswordById(u.ID, string(hashedNewPassword)) +} diff --git a/site-portal/server/domain/repo/job_participant_repo.go b/site-portal/server/domain/repo/job_participant_repo.go new file mode 100644 index 00000000..15978a96 --- /dev/null +++ b/site-portal/server/domain/repo/job_participant_repo.go @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrJobParticipantNotFound is an error returned when no participant record is found +var ErrJobParticipantNotFound = errors.New("this site is not in the current job") + +// JobParticipantRepository is the interface for managing job participant in the repo +type JobParticipantRepository interface { + // Create takes an *entity.JobParticipant and save it in the repo + Create(interface{}) error + // UpdateStatusByUUID takes an *entity.JobParticipant and updates the status in the repo + UpdateStatusByUUID(interface{}) error + // GetByJobAndSiteUUID returns an *entity.JobParticipant indexed by the job and site uuid + GetByJobAndSiteUUID(string, string) (interface{}, error) + // GetListByJobUUID returns an []entity.JobParticipant list in the specified job + GetListByJobUUID(string) (interface{}, error) +} diff --git a/site-portal/server/domain/repo/job_repo.go b/site-portal/server/domain/repo/job_repo.go new file mode 100644 index 00000000..08878a0b --- /dev/null +++ b/site-portal/server/domain/repo/job_repo.go @@ -0,0 +1,48 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrJobNotFound is the error returned when no job is found +var ErrJobNotFound = errors.New("job not found") + +// JobRepository is the interface to manage job info in the repo +type JobRepository interface { + // Create takes an *entity.Job and creates it in the repo + Create(interface{}) error + // UpdateFATEJobInfoByUUID takes an *entity.Job and updates the FATE job related info + UpdateFATEJobInfoByUUID(interface{}) error + // UpdateFATEJobStatusByUUID takes an *entity.Job and updates the FATE job status field + UpdateFATEJobStatusByUUID(interface{}) error + // UpdateStatusByUUID takes an *entity.Job and updates the job status field + UpdateStatusByUUID(interface{}) error + // UpdateStatusMessageByUUID takes an *entity.Job and updates the job status message field + UpdateStatusMessageByUUID(interface{}) error + // UpdateFinishTimeByUUID takes an *entity.Job and updates the finish time + UpdateFinishTimeByUUID(interface{}) error + // UpdateResultInfoByUUID takes an *entity.Job and updates the result info + UpdateResultInfoByUUID(interface{}) error + // CheckNameConflict returns error if the same name job exists + CheckNameConflict(string) error + // GetAll returns []entity.Job of all not-deleted jobs + GetAll() (interface{}, error) + // DeleteByProjectUUID delete the job of the specified project + DeleteByProjectUUID(string) error + // GetListByProjectUUID returns a list of []entity.Job in the specified project + GetListByProjectUUID(string) (interface{}, error) + // GetByUUID returns an *entity.Job of the specified uuid + GetByUUID(string) (interface{}, error) +} diff --git a/site-portal/server/domain/repo/local_data_repo.go b/site-portal/server/domain/repo/local_data_repo.go new file mode 100644 index 00000000..71a2953a --- /dev/null +++ b/site-portal/server/domain/repo/local_data_repo.go @@ -0,0 +1,35 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// LocalDataRepository holds methods to access local data repos +// The interface{} parameters in most of the functions should of type "*entity.LocalData" +type LocalDataRepository interface { + // Create save the data records, the interface should of type "*entity.LocalData" + Create(interface{}) error + // UpdateJobInfoByUUID changes the data upload job status + UpdateJobInfoByUUID(interface{}) error + // GetAll returns all the uploaded local data + // the returned interface{} should be of type []entity.LocalData + GetAll() (interface{}, error) + // LoadByUUID returns a *entity.LocalData object by providing the uuid + GetByUUID(string) (interface{}, error) + // DeleteByUUID deletes the data record + DeleteByUUID(string) error + // UpdateIDMetaInfoByUUID updates the IDMetaInfo, the passed interface{} is of type entity.IDMetaInfo + UpdateIDMetaInfoByUUID(string, interface{}) error + // CheckNameConflict returns an error if there are name conflicts + CheckNameConflict(string) error +} diff --git a/site-portal/server/domain/repo/model_deploymentt_repo.go b/site-portal/server/domain/repo/model_deploymentt_repo.go new file mode 100644 index 00000000..4b6d2cd3 --- /dev/null +++ b/site-portal/server/domain/repo/model_deploymentt_repo.go @@ -0,0 +1,24 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +type ModelDeploymentRepository interface { + // Create takes an *entity.ModelDeployment and save it into the repo + Create(interface{}) error + // UpdateStatusByUUID takes an *entity.ModelDeployment and update the status in the repo using uuid as index + UpdateStatusByUUID(interface{}) error + // UpdateResultJsonByUUID takes an *entity.ModelDeployment and update the resultJson in the repo using uuid as index + UpdateResultJsonByUUID(interface{}) error +} diff --git a/site-portal/server/domain/repo/model_repo.go b/site-portal/server/domain/repo/model_repo.go new file mode 100644 index 00000000..33c2f7bb --- /dev/null +++ b/site-portal/server/domain/repo/model_repo.go @@ -0,0 +1,34 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrModelNotFound is the error returned when no model is found +var ErrModelNotFound = errors.New("model not found") + +// ModelRepository is the interface for managing models +type ModelRepository interface { + // Create takes an *entity.Model and save it into the repo + Create(interface{}) error + // GetAll returns []entity.Model of all not-deleted models + GetAll() (interface{}, error) + // DeleteByUUID deletes the specified model + DeleteByUUID(string) error + // GetListByProjectUUID returns []entity.Model belonging to the specified project + GetListByProjectUUID(string) (interface{}, error) + // GetByUUID returns an *entity.Model indexed by the uuid + GetByUUID(string) (interface{}, error) +} diff --git a/site-portal/server/domain/repo/project_data_repo.go b/site-portal/server/domain/repo/project_data_repo.go new file mode 100644 index 00000000..79e3a8da --- /dev/null +++ b/site-portal/server/domain/repo/project_data_repo.go @@ -0,0 +1,46 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import ( + "github.com/pkg/errors" +) + +// ErrProjectDataNotFound is an error when no data association record found +var ErrProjectDataNotFound = errors.New("this data is not in the specified project") + +// ProjectDataRepository is the repo interface for persisting the project data info +type ProjectDataRepository interface { + // Create takes an *entity.ProjectData and create the records + Create(interface{}) error + // GetByProjectAndDataUUID returns an *entity.ProjectData indexed by the specified project and data uuid + GetByProjectAndDataUUID(string, string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.ProjectData and update its status + UpdateStatusByUUID(interface{}) error + // GetListByProjectUUID returns []entity.ProjectData that associated in the specified project + GetListByProjectUUID(string) (interface{}, error) + // GetListByProjectAndSiteUUID returns []entity.ProjectData that associated in the specified project by the specified site + GetListByProjectAndSiteUUID(string, string) (interface{}, error) + // DeleteByUUID delete the project data records by the specified uuid + DeleteByUUID(string) error + // DeleteByProjectUUID delete the project data records permanently by the specified project uuid + DeleteByProjectUUID(string) error + // UpdateSiteInfoBySiteUUID takes an *entity.ProjectData as template to update site info of the specified site uuid + UpdateSiteInfoBySiteUUID(interface{}) error + // GetListByDataUUID returns []entity.ProjectData that contains the associated local data + GetListByDataUUID(string) (interface{}, error) + // GetByDataUUID returns an *entity.ProjectData that has the specified data uuid, it is just used for retrieving data info + GetByDataUUID(string) (interface{}, error) +} diff --git a/site-portal/server/domain/repo/project_invitation_repo.go b/site-portal/server/domain/repo/project_invitation_repo.go new file mode 100644 index 00000000..d171f428 --- /dev/null +++ b/site-portal/server/domain/repo/project_invitation_repo.go @@ -0,0 +1,30 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// ProjectInvitationRepository is an interface for working with the invitation repo +type ProjectInvitationRepository interface { + // Create takes an *entity.ProjectInvitation to create the record + Create(interface{}) error + // UpdateStatusByUUID takes an *entity.ProjectInvitation and updates the status + UpdateStatusByUUID(interface{}) error + // GetByProjectUUID returns an *entity.ProjectInvitation for the specified project. it is the latest one for the project + GetByProjectUUID(string) (interface{}, error) + // GetByUUID returns an *entity.ProjectInvitation indexed by the specified uuid + GetByUUID(string) (interface{}, error) + // GetByProjectAndSiteUUID returns the latest *entity.ProjectInvitation for the specified site and project, + // used by the managing site + GetByProjectAndSiteUUID(string, string) (interface{}, error) +} diff --git a/site-portal/server/domain/repo/project_participant_repo.go b/site-portal/server/domain/repo/project_participant_repo.go new file mode 100644 index 00000000..735b7976 --- /dev/null +++ b/site-portal/server/domain/repo/project_participant_repo.go @@ -0,0 +1,39 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrProjectParticipantNotFound is an error returned when no participant record is found +var ErrProjectParticipantNotFound = errors.New("this site is not in the current project") + +// ProjectParticipantRepository is the interface to work with participant related repo +type ProjectParticipantRepository interface { + // CountJoinedParticipantByProjectUUID returns number of participants in joined status + CountJoinedParticipantByProjectUUID(string) (int64, error) + // GetByProjectUUID returns a []entity.ProjectParticipant in the specified project + GetByProjectUUID(string) (interface{}, error) + // Create takes an *entity.ProjectParticipant cam create the records + Create(interface{}) error + // GetByProjectAndSiteUUID returns an *entity.ProjectParticipant from the specified project and site uuid + GetByProjectAndSiteUUID(string, string) (interface{}, error) + // UpdateStatusByUUID takes an *entity.ProjectParticipant and update its status + UpdateStatusByUUID(interface{}) error + // DeleteByProjectUUID delete the records specified by the uui + DeleteByProjectUUID(string) error + // UpdateParticipantInfoBySiteUUID takes an *entity.ProjectParticipant as template and + // updates sites info of the records containing the specified site uuid + UpdateParticipantInfoBySiteUUID(interface{}) error +} diff --git a/site-portal/server/domain/repo/project_repo.go b/site-portal/server/domain/repo/project_repo.go new file mode 100644 index 00000000..caf1d740 --- /dev/null +++ b/site-portal/server/domain/repo/project_repo.go @@ -0,0 +1,43 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/pkg/errors" + +// ErrProjectNotFound is the error returned when no project is found +var ErrProjectNotFound = errors.New("project not found") + +// ProjectRepository is the repo interface for project +type ProjectRepository interface { + // Create takes an *entity.Project and create the record + Create(interface{}) error + // GetAll returns an []entity.Project + GetAll() (interface{}, error) + // GetByUUID returns an *entity.Project with the specified uuid + GetByUUID(string) (interface{}, error) + // DeleteByUUID deletes the project + DeleteByUUID(string) error + // CheckNameConflict returns an error if there are name conflicts + CheckNameConflict(string) error + // UpdateStatusByUUID takes an *entity.Project and update its status + UpdateStatusByUUID(interface{}) error + // UpdateTypeByUUID takes an *entity.Project and update its type + UpdateTypeByUUID(interface{}) error + // UpdateAutoApprovalStatusByUUID takes an *entity.Project and update its auto-approval status + UpdateAutoApprovalStatusByUUID(interface{}) error + // UpdateManagingSiteInfoBySiteUUID takes an *entity.Project as template and + // updates site related info of all records containing the site uuid + UpdateManagingSiteInfoBySiteUUID(interface{}) error +} diff --git a/site-portal/server/domain/repo/site_repo.go b/site-portal/server/domain/repo/site_repo.go new file mode 100644 index 00000000..2fa6d364 --- /dev/null +++ b/site-portal/server/domain/repo/site_repo.go @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +// SiteRepository is the interface to handle site related information in the repo +type SiteRepository interface { + // Load site information from the underlying layer + Load(instance interface{}) error + // Update site information + Update(instance interface{}) error + // UpdateFMLManagerConnectionStatus updates fml manager related information + UpdateFMLManagerConnectionStatus(instance interface{}) error +} diff --git a/site-portal/server/domain/repo/user_repo.go b/site-portal/server/domain/repo/user_repo.go new file mode 100644 index 00000000..3d5896f5 --- /dev/null +++ b/site-portal/server/domain/repo/user_repo.go @@ -0,0 +1,33 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repo + +import "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + +// UserRepository holds methods to access user repos +type UserRepository interface { + // GetAllUsers returns all the saved users + GetAllUsers() (interface{}, error) + // CreateUser create a new user + CreateUser(user interface{}) error + // UpdatePermissionInfoById updates a users permission + UpdatePermissionInfoById(id uint, info valueobject.UserPermissionInfo) error + // LoadById loads info of a user from the repo + LoadById(user interface{}) error + // LoadByName loads info of a user from the repo + LoadByName(user interface{}) error + //UpdatePasswordById updates a users password + UpdatePasswordById(id uint, newPassword string) error +} diff --git a/site-portal/server/domain/service/model_service.go b/site-portal/server/domain/service/model_service.go new file mode 100644 index 00000000..df0ffa24 --- /dev/null +++ b/site-portal/server/domain/service/model_service.go @@ -0,0 +1,134 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "encoding/json" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" +) + +// ModelService provides domain service functions to work with trained models +type ModelService struct { + ModelRepo repo.ModelRepository + ModelDeploymentRepo repo.ModelDeploymentRepository +} + +// ModelDeploymentRequest is a request to deploy a model +type ModelDeploymentRequest struct { + ServiceName string `json:"service_name"` + Type entity.ModelDeploymentType `json:"deployment_type"` + UserParametersJson string `json:"parameters_json"` + ModelUUID string `json:"-"` + FATEFlowContext entity.FATEFlowContext `json:"-"` + KubeflowConfig valueobject.KubeflowConfig `json:"-"` +} + +// DeployModel deploy the requested model to the requested platform +func (s *ModelService) DeployModel(request *ModelDeploymentRequest) (*entity.ModelDeployment, error) { + model, err := s.loadModel(request.ModelUUID) + if err != nil { + return nil, err + } + + // validate + supportedTypes, err := s.GetSupportedDeploymentType(request.ModelUUID) + if err != nil { + return nil, err + } + if supported := func(t entity.ModelDeploymentType) bool { + for _, supportedType := range supportedTypes { + if t == supportedType { + return true + } + } + return false + }(request.Type); !supported { + return nil, errors.Errorf("cannot deploy model to the specified deployment type: %v", request.Type) + } + + if err := request.KubeflowConfig.Validate(); err != nil { + return nil, errors.Wrapf(err, "failed to validate kubeflow configuration") + } + + // create the deployment object + requestJsonByte, err := json.MarshalIndent(request, "", " ") + if err != nil { + return nil, err + } + requestJson := string(requestJsonByte) + + deploymentParamsJson, err := valueobject.GetKFServingDeploymentParametersJson(request.UserParametersJson, request.KubeflowConfig) + if err != nil { + return nil, err + } + + modelDeployment := &entity.ModelDeployment{ + UUID: uuid.NewV4().String(), + ServiceName: request.ServiceName, + ModelUUID: request.ModelUUID, + Type: request.Type, + Status: entity.ModelDeploymentStatusCreated, + DeploymentParametersJson: deploymentParamsJson, + RequestJson: requestJson, + ResultJson: "", + Repo: s.ModelDeploymentRepo, + } + if err := s.ModelDeploymentRepo.Create(modelDeployment); err != nil { + return nil, err + } + + // deploy it! + if err := modelDeployment.Deploy(entity.ModelDeploymentContext{ + Model: model, + FATEFlowContext: request.FATEFlowContext, + }); err != nil { + modelDeployment.Status = entity.ModelDeploymentStatusFailed + if updateErr := s.ModelDeploymentRepo.UpdateStatusByUUID(modelDeployment); updateErr != nil { + log.Err(updateErr).Msg("failed to update deployment status") + } + return nil, err + } + return modelDeployment, nil +} + +// GetSupportedDeploymentType returns a list of entity.ModelDeploymentType that the specified model can be deployed to +func (s *ModelService) GetSupportedDeploymentType(modelUUID string) ([]entity.ModelDeploymentType, error) { + model, err := s.loadModel(modelUUID) + if err != nil { + return nil, err + } + switch model.ComponentAlgorithmType { + case entity.ComponentAlgorithmTypeHomoSBT, entity.ComponentAlgorithmTypeHomoLR: + return []entity.ModelDeploymentType{entity.ModelDeploymentTypeKFServing}, nil + default: + return nil, errors.Errorf("unsupported component type: %v", model.ComponentAlgorithmType) + } +} + +func (s *ModelService) loadModel(modelUUID string) (*entity.Model, error) { + modelInstance, err := s.ModelRepo.GetByUUID(modelUUID) + if err != nil { + return nil, errors.Wrap(err, "failed to query model") + } + model := modelInstance.(*entity.Model) + model.Repo = s.ModelRepo + return model, nil +} diff --git a/site-portal/server/domain/service/project_service.go b/site-portal/server/domain/service/project_service.go new file mode 100644 index 00000000..f1de50eb --- /dev/null +++ b/site-portal/server/domain/service/project_service.go @@ -0,0 +1,337 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "strconv" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/domain/aggregate" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/event" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/fmlmanager" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// ProjectService provides domain services for the project management context +// We created this because we are not sure if they can be put into the "aggregate" +// or the "entity" package - seems they are not owned by any entity. +// This can be revisited in the future. +type ProjectService struct { + ProjectRepo repo.ProjectRepository + ParticipantRepo repo.ProjectParticipantRepository + InvitationRepo repo.ProjectInvitationRepository + ProjectDataRepo repo.ProjectDataRepository +} + +// ProcessProjectAcceptance process invitation acceptance response +func (s *ProjectService) ProcessProjectAcceptance(invitationUUID string) error { + invitationInstance, err := s.InvitationRepo.GetByUUID(invitationUUID) + if err != nil { + return err + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusAccepted + participantInstance, err := + s.ParticipantRepo.GetByProjectAndSiteUUID(invitation.ProjectUUID, invitation.SiteUUID) + if err != nil { + return err + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusJoined + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return err + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + return nil +} + +// ProcessProjectRejection process invitation rejection response +func (s *ProjectService) ProcessProjectRejection(invitationUUID string) error { + invitationInstance, err := s.InvitationRepo.GetByUUID(invitationUUID) + if err != nil { + return err + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRejected + participantInstance, err := + s.ParticipantRepo.GetByProjectAndSiteUUID(invitation.ProjectUUID, invitation.SiteUUID) + if err != nil { + return err + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusRejected + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return err + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + return nil +} + +// ProcessInvitationRevocation removes the project and updates invitation status +func (s *ProjectService) ProcessInvitationRevocation(invitationUUID string) error { + invitationInstance, err := s.InvitationRepo.GetByUUID(invitationUUID) + if err != nil { + return err + } + invitation := invitationInstance.(*entity.ProjectInvitation) + if invitation.Status != entity.ProjectInvitationStatusSent { + return errors.Errorf("invalide invitation status: %d", invitation.Status) + } + invitation.Status = entity.ProjectInvitationStatusRevoked + + // delete the project + if err := s.ProjectRepo.DeleteByUUID(invitation.ProjectUUID); err != nil { + return err + } + if err := s.ParticipantRepo.DeleteByProjectUUID(invitation.ProjectUUID); err != nil { + return err + } + if err := s.ProjectDataRepo.DeleteByProjectUUID(invitation.ProjectUUID); err != nil { + return err + } + if err := s.InvitationRepo.UpdateStatusByUUID(invitation); err != nil { + return err + } + return nil +} + +// ProcessParticipantDismissal processes participant dismissal event by updating the repo records +func (s *ProjectService) ProcessParticipantDismissal(projectUUID, siteUUID string, isCurrentSite bool) error { + // dismiss data association + dataListInstance, err := s.ProjectDataRepo.GetListByProjectAndSiteUUID(projectUUID, siteUUID) + if err != nil { + return errors.Wrap(err, "failed to query project data") + } + dataList := dataListInstance.([]entity.ProjectData) + for _, data := range dataList { + if data.Status == entity.ProjectDataStatusAssociated { + data.Status = entity.ProjectDataStatusDismissed + if err := s.ProjectDataRepo.UpdateStatusByUUID(&data); err != nil { + return errors.Wrapf(err, "failed to dismiss data %s from site: %s", data.Name, data.SiteName) + } + } + } + // update participant status + participantInstance, err := s.ParticipantRepo.GetByProjectAndSiteUUID(projectUUID, siteUUID) + if err != nil { + return err + } + participant := participantInstance.(*entity.ProjectParticipant) + participant.Status = entity.ProjectParticipantStatusDismissed + if err := s.ParticipantRepo.UpdateStatusByUUID(participant); err != nil { + return err + } + // mark project as dismissed for the dismissed site + if isCurrentSite { + projectInstance, err := s.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return err + } + project := projectInstance.(*entity.Project) + project.Status = entity.ProjectStatusDismissed + log.Info().Msgf("current site is dismissed from the project: %s(%s)", project.Name, project.UUID) + return s.ProjectRepo.UpdateStatusByUUID(project) + } + return nil +} + +// ProcessProjectClosing processes project closing event by update the repo records +func (s *ProjectService) ProcessProjectClosing(projectUUID string) error { + projectInstance, err := s.ProjectRepo.GetByUUID(projectUUID) + if err != nil { + return err + } + project := projectInstance.(*entity.Project) + project.Status = entity.ProjectStatusClosed + + if project.Type != entity.ProjectTypeRemote { + return errors.New("project is not managed by other site") + } + + if err := s.ProjectRepo.UpdateStatusByUUID(project); err != nil { + return errors.Wrapf(err, "failed to update project status") + } + return nil +} + +// ProcessProjectSyncRequest sync project info from fml manager and may updates data and participant info if needed +func (s *ProjectService) ProcessProjectSyncRequest(context *aggregate.ProjectSyncContext) error { + if !context.FMLManagerConnectionInfo.Connected { + log.Warn().Msg("ProcessProjectSyncRequest: FML manager is not connected") + return nil + } + + log.Info().Msg("start syncing project list...") + client := fmlmanager.NewFMLManagerClient(context.FMLManagerConnectionInfo.Endpoint, context.FMLManagerConnectionInfo.ServerName) + projectMap, err := client.GetProject(context.LocalSiteUUID) + if err != nil { + return err + } + + projectListInstance, err := s.ProjectRepo.GetAll() + if err != nil { + return err + } + projectList := projectListInstance.([]entity.Project) + + for _, project := range projectList { + // only sync projects managed by other sites + if project.Type != entity.ProjectTypeRemote { + delete(projectMap, project.UUID) + continue + } + + if remoteProject, ok := projectMap[project.UUID]; ok { + if project.Status != entity.ProjectStatus(remoteProject.ProjectStatus) { + project.Status = entity.ProjectStatus(remoteProject.ProjectStatus) + log.Warn().Msgf("changing stale project status, project: %s, status: %v", project.UUID, project.Status) + if err := s.ProjectRepo.UpdateStatusByUUID(&project); err != nil { + return errors.Wrapf(err, "failed to update project (%s) status", project.UUID) + } + // TODO: figure out if we need to sync data and participant + } + delete(projectMap, project.UUID) + } else { + log.Warn().Msgf("removing stale project %s", project.UUID) + if err := s.ProjectRepo.DeleteByUUID(project.UUID); err != nil { + return errors.Wrapf(err, "failed to delete project (%s)", project.UUID) + } + if err := s.ParticipantRepo.DeleteByProjectUUID(project.UUID); err != nil { + return errors.Wrapf(err, "failed to delete participants from project %s", project.UUID) + } + if err := s.ProjectDataRepo.DeleteByProjectUUID(project.UUID); err != nil { + return errors.Wrapf(err, "failed to delete project data from project %s", project.UUID) + } + } + } + + for _, remoteProject := range projectMap { + // ignore projects managed by current site + if remoteProject.ProjectManagingSiteUUID == context.LocalSiteUUID || + entity.ProjectStatus(remoteProject.ProjectStatus) == entity.ProjectStatusManaged { + continue + } + // XXX: this project is newly added but we don't know, but this should never happen, at least if the project is in joined status + log.Warn().Msgf("adding project %v", remoteProject) + } + return nil +} + +// ProjectSyncService contains project sync status and provides methods for ensuring projects are synced +// TODO: we should use db data to record each project's last sync time +type ProjectSyncService struct { + disabled bool + listSyncedAt time.Time + projectDataSyncedAt map[string]time.Time + projectParticipantSyncedAt map[string]time.Time + interval time.Duration +} + +// NewProjectSyncService returns a syncing service instance +func NewProjectSyncService() *ProjectSyncService { + disabled, _ := strconv.ParseBool(viper.GetString("siteportal.project.sync.disabled")) + intervalInt, err := strconv.Atoi(viper.GetString("siteportal.project.sync.interval")) + if err != nil || intervalInt == 0 { + intervalInt = 20 + } + return &ProjectSyncService{ + disabled: disabled, + listSyncedAt: time.Time{}, + projectDataSyncedAt: map[string]time.Time{}, + projectParticipantSyncedAt: map[string]time.Time{}, + interval: time.Duration(intervalInt) * time.Minute, + } +} + +// EnsureProjectListSynced makes sure the project list is synced +func (s *ProjectSyncService) EnsureProjectListSynced() error { + if s.disabled { + return nil + } + if s.listSyncedAt.Add(s.interval).Before(time.Now()) { + s.listSyncedAt = time.Now() + exchange := event.NewSelfHttpExchange() + if err := exchange.PostEvent(event.ProjectListSyncEvent{}); err != nil { + return err + } + } + return nil +} + +// EnsureProjectDataSynced makes sure the data association info in a project is synced +func (s *ProjectSyncService) EnsureProjectDataSynced(projectUUID string) error { + if s.disabled { + return nil + } + if syncedAt, ok := s.projectDataSyncedAt[projectUUID]; !ok || syncedAt.Add(s.interval).Before(time.Now()) { + s.projectDataSyncedAt[projectUUID] = time.Now() + exchange := event.NewSelfHttpExchange() + if err := exchange.PostEvent(event.ProjectDataSyncEvent{ + ProjectUUID: projectUUID, + }); err != nil { + return err + } + } + return nil +} + +// EnsureProjectParticipantSynced makes sure the participants' info in a project is synced +func (s *ProjectSyncService) EnsureProjectParticipantSynced(projectUUID string) error { + if s.disabled { + return nil + } + if syncedAt, ok := s.projectParticipantSyncedAt[projectUUID]; !ok || syncedAt.Add(s.interval).Before(time.Now()) { + s.projectParticipantSyncedAt[projectUUID] = time.Now() + exchange := event.NewSelfHttpExchange() + if err := exchange.PostEvent(event.ProjectParticipantSyncEvent{ + ProjectUUID: projectUUID, + }); err != nil { + return err + } + } + return nil +} + +// CleanupProject remove stale project map item +func (s *ProjectSyncService) CleanupProject(joinedProjects map[string]interface{}) error { + for k := range s.projectDataSyncedAt { + if _, ok := joinedProjects[k]; !ok { + delete(s.projectDataSyncedAt, k) + delete(s.projectParticipantSyncedAt, k) + log.Info().Str("entity", "ProjectSyncService").Msgf("removed stale project %s", k) + } + } + for k := range s.projectParticipantSyncedAt { + if _, ok := joinedProjects[k]; !ok { + delete(s.projectParticipantSyncedAt, k) + log.Info().Str("entity", "ProjectSyncService").Msgf("removed stale project %s", k) + } + } + return nil +} diff --git a/site-portal/server/domain/service/user_service.go b/site-portal/server/domain/service/user_service.go new file mode 100644 index 00000000..635335df --- /dev/null +++ b/site-portal/server/domain/service/user_service.go @@ -0,0 +1,52 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "golang.org/x/crypto/bcrypt" +) + +// UserService provides common services to work with entity.User +type UserService struct { + Repo repo.UserRepository +} + +// ErrAccessDenied means the user cannot access Site Portal due to missing permissions +var ErrAccessDenied = errors.New("this user doesn't have Site Portal access") + +// LoginService validates the provided username and password and return the user entity when succeed +func (s *UserService) LoginService(username, password string) (*entity.User, error) { + user := &entity.User{Name: username} + if err := func() error { + if err := s.Repo.LoadByName(user); err != nil { + return err + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return err + } + if !user.PermissionInfo.SitePortalAccess { + return ErrAccessDenied + } + return nil + }(); err != nil { + log.Warn().Err(err).Msgf("failed to validate password for user: %s", username) + return nil, err + } + return user, nil +} diff --git a/site-portal/server/domain/valueobject/algorithm_config.go b/site-portal/server/domain/valueobject/algorithm_config.go new file mode 100644 index 00000000..1bbd416b --- /dev/null +++ b/site-portal/server/domain/valueobject/algorithm_config.go @@ -0,0 +1,36 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + "encoding/json" +) + +// AlgorithmConfig contains some configurations for running a training job +type AlgorithmConfig struct { + TrainingValidationEnabled bool `json:"training_validation_enabled"` + TrainingValidationSizePercent uint `json:"training_validation_percent"` + TrainingComponentsToDeploy []string `json:"training_component_list_to_deploy"` +} + +func (c AlgorithmConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *AlgorithmConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} diff --git a/site-portal/server/domain/valueobject/id_meta_info.go b/site-portal/server/domain/valueobject/id_meta_info.go new file mode 100644 index 00000000..3752d09a --- /dev/null +++ b/site-portal/server/domain/valueobject/id_meta_info.go @@ -0,0 +1,55 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + "encoding/json" +) + +// IDType is the type of the ID +type IDType uint8 + +const ( + IDTypeOther IDType = iota + IDTypeCellPhone + IDTypeDeviceIMEI + IDTypeDeviceIDFA + IDTypeDeviceIDFV +) + +// IDEncryptionType is the encryption type of the ID field +type IDEncryptionType uint8 + +const ( + IDEncryptionTypeNone IDEncryptionType = iota + IDEncryptionTypeMD5 + IDEncryptionTypeSHA256 +) + +// IDMetaInfo records the metadata describing certain data +type IDMetaInfo struct { + IDType IDType `json:"id_type"` + IDEncryptionType IDEncryptionType `json:"id_encryption_type"` +} + +func (i IDMetaInfo) Value() (driver.Value, error) { + bJson, err := json.Marshal(i) + return bJson, err +} + +func (i *IDMetaInfo) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), i) +} diff --git a/site-portal/server/domain/valueobject/kfserving_deployment_parameter.go b/site-portal/server/domain/valueobject/kfserving_deployment_parameter.go new file mode 100644 index 00000000..8897e6f9 --- /dev/null +++ b/site-portal/server/domain/valueobject/kfserving_deployment_parameter.go @@ -0,0 +1,97 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "encoding/json" + "github.com/pkg/errors" +) + +// KFServingParameter is the parameters used for deploying to KFServing +type KFServingParameter struct { + ProtocolVersion string `json:"protocol_version"` + KubeconfigContent string `json:"config_file_content"` + Namespace string `json:"namespace"` + Replace bool `json:"replace"` + SKipCreateStorageSecret bool `json:"skip_create_storage_secret"` + ModelStorageType string `json:"model_storage_type"` +} + +// KFServingWithMinIOParameter is the parameters used for deploying to KFServing +type KFServingWithMinIOParameter struct { + KFServingParameter + ModelStorageParameters MinIOStorageMParameters `json:"model_storage_parameters"` +} + +// MinIOStorageMParameters is the parameters for the minio storage +type MinIOStorageMParameters struct { + Endpoint string `json:"endpoint"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + Secure bool `json:"secure"` + Region string `json:"region"` +} + +// GetKFServingDeploymentParametersJson returns a deployment parameter json +func GetKFServingDeploymentParametersJson(userParametersJson string, kubeflowConfig KubeflowConfig) (string, error) { + var userSpecifiedParam KFServingParameter + if userParametersJson != "" { + if err := json.Unmarshal([]byte(userParametersJson), &userSpecifiedParam); err != nil { + return "", err + } + if userSpecifiedParam.ModelStorageType != "" && userSpecifiedParam.ModelStorageType != "minio" { + return "", errors.Errorf("not supported storage type: %s", userSpecifiedParam.ModelStorageType) + } + } + + // the default param + defaultParam := &KFServingWithMinIOParameter{ + KFServingParameter: KFServingParameter{ + ProtocolVersion: "v1", + KubeconfigContent: kubeflowConfig.KubeConfig, + Namespace: "default", + Replace: false, + SKipCreateStorageSecret: false, + ModelStorageType: "minio", + }, + ModelStorageParameters: MinIOStorageMParameters{ + Endpoint: kubeflowConfig.MinIOEndpoint, + AccessKey: kubeflowConfig.MinIOAccessKey, + SecretKey: kubeflowConfig.MinIOSecretKey, + Secure: kubeflowConfig.MinIOSSLEnabled, + Region: kubeflowConfig.MinIORegion, + }, + } + + // merge the user input + mergedJson, err := json.Marshal(defaultParam) + if err != nil { + return "", err + } + var m map[string]interface{} + if err := json.Unmarshal(mergedJson, &m); err != nil { + return "", err + } + if userParametersJson != "" { + if err := json.Unmarshal([]byte(userParametersJson), &m); err != nil { + return "", errors.Wrapf(err, "failed to merge the user specified params") + } + mergedJson, err = json.MarshalIndent(m, "", " ") + if err != nil { + return "", err + } + } + return string(mergedJson), nil +} diff --git a/site-portal/server/domain/valueobject/kubeflow_config.go b/site-portal/server/domain/valueobject/kubeflow_config.go new file mode 100644 index 00000000..8831ad22 --- /dev/null +++ b/site-portal/server/domain/valueobject/kubeflow_config.go @@ -0,0 +1,115 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "context" + "database/sql/driver" + "encoding/json" + "io/ioutil" + "os" + + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/kubernetes" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// KubeflowConfig contains necessary information needed to deploy model to Kubeflow +type KubeflowConfig struct { + // MinIOEndpoint is the address for the MinIO server + MinIOEndpoint string `json:"minio_endpoint"` + // MinIOAccessKey is the access-key for the MinIO server + MinIOAccessKey string `json:"minio_access_key"` + // MinIOSecretKey is the secret-key for the MinIO server + MinIOSecretKey string `json:"minio_secret_key"` + // MinIOSSLEnabled is whether this connection should be over ssl + MinIOSSLEnabled bool `json:"minio_ssl_enabled"` + // MinIORegion is the region of the MinIO service + MinIORegion string `json:"minio_region"` + // KubeConfig is the content of the kubeconfig file to connect to kubernetes + KubeConfig string `json:"kubeconfig"` +} + +func (c KubeflowConfig) Value() (driver.Value, error) { + bJson, err := json.Marshal(c) + return bJson, err +} + +func (c *KubeflowConfig) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), c) +} + +func (c *KubeflowConfig) getTempKubeconfigFile() (string, error) { + if c.KubeConfig == "" { + // return empty file path so the client will try the in-cluster config + return "", nil + } + + tmpFile, err := ioutil.TempFile(os.TempDir(), "kubeconfig-") + if err != nil { + return "", err + } + defer tmpFile.Close() + text := []byte(c.KubeConfig) + if _, err = tmpFile.Write(text); err != nil { + return "", err + } + return tmpFile.Name(), nil +} + +// Validate checks the connection to the kubernetes, the installation of KFServing and the connection to MinIO +func (c *KubeflowConfig) Validate() error { + if err := func() error { + kubeconfigFile, err := c.getTempKubeconfigFile() + if err != nil { + return err + } + client, err := kubernetes.NewKubernetesClient(kubeconfigFile) + if err != nil { + return err + } + list, err := client.GetInferenceServiceList() + if err != nil { + return err + } + log.Debug().Msgf("got isvc list %s", list) + return nil + }(); err != nil { + return errors.Wrap(err, "failed to check KFServing installation") + } + if err := c.validateMinIO(); err != nil { + return errors.Wrap(err, "failed to check MinIO connection") + } + return nil +} + +func (c *KubeflowConfig) validateMinIO() error { + client, err := minio.New(c.MinIOEndpoint, &minio.Options{ + Creds: credentials.NewStaticV4(c.MinIOAccessKey, c.MinIOSecretKey, ""), + Secure: c.MinIOSSLEnabled, + Region: c.MinIORegion, + }) + if err != nil { + return err + } + // seems there is no "test connection" API, just test a dummy bucket and ignore the response + _, err = client.BucketExists(context.TODO(), "check") + if err != nil { + return err + } + return nil +} diff --git a/site-portal/server/domain/valueobject/model_evaluation.go b/site-portal/server/domain/valueobject/model_evaluation.go new file mode 100644 index 00000000..8db616e6 --- /dev/null +++ b/site-portal/server/domain/valueobject/model_evaluation.go @@ -0,0 +1,32 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +import ( + "database/sql/driver" + "encoding/json" +) + +// ModelEvaluation is a key-value pair of the model evaluation metrics +type ModelEvaluation map[string]string + +func (i ModelEvaluation) Value() (driver.Value, error) { + bJson, err := json.Marshal(i) + return bJson, err +} + +func (i *ModelEvaluation) Scan(v interface{}) error { + return json.Unmarshal([]byte(v.(string)), i) +} diff --git a/site-portal/server/domain/valueobject/project_creator_info.go b/site-portal/server/domain/valueobject/project_creator_info.go new file mode 100644 index 00000000..17c05fdd --- /dev/null +++ b/site-portal/server/domain/valueobject/project_creator_info.go @@ -0,0 +1,23 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +// ProjectCreatorInfo contains info of the site managing/creating the project +type ProjectCreatorInfo struct { + Manager string `json:"manager" gorm:"type:varchar(255)"` + ManagingSiteName string `json:"managing_site_name" gorm:"type:varchar(255)"` + ManagingSitePartyID uint `json:"managing_site_party_id"` + ManagingSiteUUID string `json:"managing_site_uuid" gorm:"type:varchar(36)"` +} diff --git a/site-portal/server/domain/valueobject/user_permission.go b/site-portal/server/domain/valueobject/user_permission.go new file mode 100644 index 00000000..fdfa0e47 --- /dev/null +++ b/site-portal/server/domain/valueobject/user_permission.go @@ -0,0 +1,25 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package valueobject + +// UserPermissionInfo holds information about user's permissions +type UserPermissionInfo struct { + // SitePortalAccess controls whether the user can access site portal + SitePortalAccess bool `json:"site_portal_access"` + // FATEBoardAccess controls whether the user can access fate board + FATEBoardAccess bool `json:"fateboard_access" gorm:"column:fateboard_access"` + // NoteBookAccess controls whether the user can access notebook + NotebookAccess bool `json:"notebook_access"` +} diff --git a/site-portal/server/infrastructure/event/exchange.go b/site-portal/server/infrastructure/event/exchange.go new file mode 100644 index 00000000..a6405cbe --- /dev/null +++ b/site-portal/server/infrastructure/event/exchange.go @@ -0,0 +1,140 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package event + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// Exchange is an interface to receive and post event +type Exchange interface { + // PostEvent sends an event + PostEvent(event Event) error +} + +type selfHttpExchange struct { + endpoint string +} + +// NewSelfHttpExchange returns an Exchange for posting event to the site itself +func NewSelfHttpExchange() *selfHttpExchange { + + tlsEnabled := viper.GetBool("siteportal.tls.enabled") + if tlsEnabled { + tlsPort := viper.GetString("siteportal.tls.port") + if tlsPort == "" { + // this is the default listening port + tlsPort = "8443" + } + return &selfHttpExchange{ + endpoint: fmt.Sprintf("https://localhost:%s", tlsPort), + } + } + // gin's default port + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + return &selfHttpExchange{ + endpoint: fmt.Sprintf("http://localhost:%s", port), + } +} + +// PostEvent calls the http API to create the event +func (e *selfHttpExchange) PostEvent(event Event) error { + urlStr := e.genURL(event.GetUrl()) + var payload []byte + payload, err := json.Marshal(event) + if err != nil { + return err + } + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + var resp *http.Response + u, err := url.Parse(urlStr) + if err != nil { + return errors.Wrap(err, "Parse URL failed") + } + // if endpoint scheme is https, use tls + if u.Scheme == "https" { + caCertPath := viper.GetString("siteportal.tls.ca.cert") + sitePortalClientCert := viper.GetString("siteportal.tls.client.cert") + sitePortalClientKey := viper.GetString("siteportal.tls.client.key") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + return errors.Wrapf(err, "read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + clientCrt, err := tls.LoadX509KeyPair(sitePortalClientCert, sitePortalClientKey) + if err != nil { + return errors.Wrapf(err, "LoadX509KeyPair error") + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{clientCrt}, + }, + } + client := &http.Client{Transport: tr} + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTPs with body %s", urlStr, string(payload))) + resp, err = client.Do(req) + if err != nil { + return err + } + } else { + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTP with body %s", urlStr, string(payload))) + resp, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + } + defer resp.Body.Close() + _, err = e.parseResponse(resp) + return err +} + +func (e *selfHttpExchange) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return nil, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + return nil, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} + +func (e *selfHttpExchange) genURL(path string) string { + return fmt.Sprintf("%s/api/v1/%s", e.endpoint, path) +} diff --git a/site-portal/server/infrastructure/event/types.go b/site-portal/server/infrastructure/event/types.go new file mode 100644 index 00000000..725438fc --- /dev/null +++ b/site-portal/server/infrastructure/event/types.go @@ -0,0 +1,78 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package event + +// Event is an interface represent a event for this site +type Event interface { + // GetUrl returns the url path for the event + GetUrl() string +} + +// ProjectParticipantUpdateEvent is an event triggered when project participant info is change +type ProjectParticipantUpdateEvent struct { + UUID string `json:"uuid"` + PartyID uint `json:"party_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func (e ProjectParticipantUpdateEvent) GetUrl() string { + return "project/internal/event/participant/update" +} + +// ModelCreationEvent is an event triggered when a modeling job is finished +type ModelCreationEvent struct { + Name string `json:"name"` + ModelID string `json:"model_id"` + ModelVersion string `json:"model_version"` + ComponentName string `json:"component_name"` + ProjectUUID string `json:"project_uuid"` + JobUUID string `json:"job_uuid"` + JobName string `json:"job_name"` + Role string `json:"role"` + PartyID uint `json:"party_id"` + Evaluation map[string]string `json:"evaluation"` + ComponentAlgorithmType uint8 `json:"algorithm_type"` +} + +func (e ModelCreationEvent) GetUrl() string { + return "model/internal/event/create" +} + +// ProjectParticipantSyncEvent is an event triggered when project participant info needs to be synced from fml manager +type ProjectParticipantSyncEvent struct { + ProjectUUID string `json:"project_uuid"` +} + +func (e ProjectParticipantSyncEvent) GetUrl() string { + return "project/internal/event/participant/sync" +} + +// ProjectDataSyncEvent is an event triggered when project data info needs to be synced from fml manager +type ProjectDataSyncEvent struct { + ProjectUUID string `json:"project_uuid"` +} + +func (e ProjectDataSyncEvent) GetUrl() string { + return "project/internal/event/data/sync" +} + +// ProjectListSyncEvent is an event triggered when project list needs to be synced +type ProjectListSyncEvent struct { +} + +func (e ProjectListSyncEvent) GetUrl() string { + return "project/internal/event/list/sync" +} diff --git a/site-portal/server/infrastructure/fateclient/client.go b/site-portal/server/infrastructure/fateclient/client.go new file mode 100644 index 00000000..b2fd2100 --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/client.go @@ -0,0 +1,526 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fateclient + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + gourl "net/url" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +type client struct { + // host address + host string + // port number + port uint + // https sets if https is enabled + https bool + // apiVersion is the version string of the api + apiVersion string +} + +// DataUploadRequest is the request to upload data +type DataUploadRequest struct { + File string `json:"file"` + Head int `json:"head"` + Partition int `json:"partition"` + WorkMode int `json:"work_mode"` + Backend int `json:"backend"` + Namespace string `json:"namespace"` + TableName string `json:"table_name"` + Drop int `json:"drop"` +} + +// ComponentTrackingCommonRequest is the request to query the metric of a component +type ComponentTrackingCommonRequest struct { + JobID string `json:"job_id"` + PartyID uint `json:"party_id"` + Role string `json:"role"` + ComponentName string `json:"component_name"` +} + +// ModelDeployRequest is the request to deploy a trained model +type ModelDeployRequest struct { + ModelInfo + ComponentList []string `json:"cpn_list"` +} + +// CommonResponse represents common response from FATE flow +type CommonResponse struct { + RetCode int `json:"retcode"` + RetMsg string `json:"retmsg"` +} + +// JobSubmissionResponseData contains returned data of job submission response +type JobSubmissionResponseData struct { + ModelInfo ModelInfo `json:"model_info"` +} + +// ModelDeployResponseData contains returned data of model deploy response +type ModelDeployResponseData struct { + ModelInfo +} + +// ModelInfo contains the key infos of a model +type ModelInfo struct { + ModelID string `json:"model_id"` + ModelVersion string `json:"model_version"` +} + +// HomoModelConversionRequest is the request to convert a homo model +type HomoModelConversionRequest struct { + ModelInfo + PartyID uint `json:"party_id"` + Role string `json:"role"` +} + +// HomoModelDeploymentRequest is the request to deploy a model to KFServing +type HomoModelDeploymentRequest struct { + HomoModelConversionRequest + ServiceID string `json:"service_id"` + ComponentName string `json:"component_name"` + DeploymentType string `json:"deployment_type"` + DeploymentParameters interface{} `json:"deployment_parameters"` +} + +// NewFATEFlowClient returns a fate flow client +func NewFATEFlowClient(host string, port uint, https bool) *client { + return &client{ + host: host, + port: port, + https: https, + } +} + +// UploadData calls the /data/upload API to upload local data to FATE flow +func (c *client) UploadData(request DataUploadRequest) (string, error) { + var b bytes.Buffer + w := multipart.NewWriter(&b) + if fw, err := w.CreateFormFile("file", request.File); err != nil { + return "", err + } else { + src, err := os.Open(request.File) + if err != nil { + return "", err + } + defer src.Close() + if _, err = io.Copy(fw, src); err != nil { + return "", err + } + } + _ = w.Close() + + args, err := json.Marshal(request) + if err != nil { + return "", err + } + url := c.genURL(fmt.Sprintf("data/upload?%s", gourl.QueryEscape(string(args)))) + req, err := http.NewRequest("POST", url, &b) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", w.FormDataContentType()) + + log.Info().Msg(fmt.Sprintf("Posting data upload request to %s", url)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", err + } + type UploadDataResponse struct { + CommonResponse + JobID string `json:"jobId"` + } + var uploadDataResponse UploadDataResponse + if err := json.Unmarshal(body, &uploadDataResponse); err != nil { + return "", err + } + if uploadDataResponse.RetCode != 0 || uploadDataResponse.JobID == "" { + responseError := errors.Errorf("failed to get job id, retmsg: %s", uploadDataResponse.RetMsg) + log.Err(responseError) + return "", responseError + } + return uploadDataResponse.JobID, nil +} + +// DeleteTable deletes a table in fate +func (c *client) DeleteTable(tableNamespace, tableName string) error { + resp, err := c.postJSON("table/delete", + fmt.Sprintf(`{"namespace": "%s", "table_name": "%s"}`, tableNamespace, tableName)) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return err + } + var tableDeletionResp CommonResponse + err = json.Unmarshal(body, &tableDeletionResp) + if err != nil { + return err + } + if tableDeletionResp.RetCode != 0 && !strings.Contains(tableDeletionResp.RetMsg, "no find table") { + return errors.Errorf("error return code: %d, msg: %s", tableDeletionResp.RetCode, tableDeletionResp.RetMsg) + } + return nil +} + +// TestConnection sends a dummy version query request to the fate flow service +func (c *client) TestConnection() error { + _, err := c.GetFATEVersion() + return err +} + +// GetFATEVersion returns the FATE-flow version +func (c *client) GetFATEVersion() (string, error) { + resp, err := c.postJSON("version/get", `{"module": "FATE"}`) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", err + } + + type versionGetResp struct { + Data map[string]string `json:"data"` + CommonResponse + } + versionInfo := new(versionGetResp) + err = json.Unmarshal(body, &versionInfo) + if err != nil { + log.Error().Err(err).Msg("unmarshal resp body error") + return "", err + } + if versionInfo.RetCode != 0 || versionInfo.Data["FATE"] == "" { + log.Error().Msgf("unexpected version query response") + return "", fmt.Errorf("unexpected version query response") + } + return versionInfo.Data["FATE"], nil +} + +// QueryJobStatus returns the job status +func (c *client) QueryJobStatus(jobID string) (string, error) { + resp, err := c.postJSON("job/query", fmt.Sprintf(`{"job_id":"%s"}`, jobID)) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", err + } + type JobStatusData struct { + JobStatus string `json:"f_status"` + } + type JobStatusResponse struct { + CommonResponse + Data []JobStatusData `json:"data"` + } + jobQueryResponse := JobStatusResponse{} + err = json.Unmarshal(body, &jobQueryResponse) + if err != nil { + return "", err + } + if jobQueryResponse.Data == nil || len(jobQueryResponse.Data) == 0 { + return "unknown", errors.Errorf("Failed to query the job with id %s", jobID) + } + return jobQueryResponse.Data[0].JobStatus, nil +} + +// SubmitJob submit a new Job +func (c *client) SubmitJob(conf, dsl string) (string, *ModelInfo, error) { + var confObj map[string]interface{} + if err := json.Unmarshal([]byte(conf), &confObj); err != nil { + return "", nil, err + } + var dslObj map[string]interface{} + if err := json.Unmarshal([]byte(dsl), &dslObj); err != nil { + return "", nil, err + } + jobSubmissionBody := map[string]interface{}{ + "job_dsl": dslObj, + "job_runtime_conf": confObj, + } + resp, err := c.postJSON("job/submit", jobSubmissionBody) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", nil, err + } + type JobSubmissionResponse struct { + CommonResponse + JobID string `json:"jobId"` + Data JobSubmissionResponseData `json:"data"` + } + var response JobSubmissionResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", nil, err + } + if response.RetCode != 0 || response.JobID == "" { + responseError := errors.Errorf("failed to get job id, retmsg: %s", response.RetMsg) + log.Err(responseError) + return "", nil, responseError + } + return response.JobID, &response.Data.ModelInfo, nil +} + +// DeployModel deploy a trained model +func (c *client) DeployModel(request ModelDeployRequest) (*ModelInfo, error) { + resp, err := c.postJSON("model/deploy", request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type ModelDeployResponse struct { + CommonResponse + Data ModelDeployResponseData `json:"data"` + } + var response ModelDeployResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to get job id, retmsg: %s", response.RetMsg) + log.Err(responseError) + return nil, responseError + } + return &response.Data.ModelInfo, nil +} + +// GetComponentMetric returns metric data for the specified component +func (c *client) GetComponentMetric(request ComponentTrackingCommonRequest) (interface{}, error) { + resp, err := c.postJSON("tracking/component/metric/all", request) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", err + } + type ComponentMetricResponse struct { + CommonResponse + Data interface{} `json:"data"` + } + var response ComponentMetricResponse + if err := json.Unmarshal(body, &response); err != nil { + return "", err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to get component metric, retmsg: %s", response.RetMsg) + log.Err(responseError) + return "", responseError + } + return response.Data, nil +} + +// GetComponentOutputDataSummary returns part of (maximum 100 lines of records) of the output data +func (c *client) GetComponentOutputDataSummary(request ComponentTrackingCommonRequest) (interface{}, interface{}, error) { + resp, err := c.postJSON("tracking/component/output/data", request) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, nil, err + } + type ComponentOutputDataResponse struct { + CommonResponse + Data interface{} `json:"data"` + Meta interface{} `json:"meta"` + } + var response ComponentOutputDataResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, nil, err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to get component output data, retmsg: %s", response.RetMsg) + log.Err(responseError) + return nil, nil, responseError + } + // response.Data should be like: + // [ + // [ + // ["542", 1, 0.049868, 1.07685, 0.004134, -0.095249, -1.155891, -0.742153, -0.53295, -0.07775, -0.289188, -0.797202], + // ... + // ] + // ] + // response.Meta should be like: + // { + // "header": [ + // ["id", "y", "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9"] + // ], + // "names": ["data"], + // "total": [551] + // } + return response.Data, response.Meta, nil +} + +// GetComponentSummary is used for getting the summary for an intersection component, it can get the intersection rate. +func (c *client) GetComponentSummary(request ComponentTrackingCommonRequest) (interface{}, error) { + resp, err := c.postJSON("tracking/component/summary/download", request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type ComponentSummaryResponse struct { + CommonResponse + Data interface{} `json:"data"` + Meta interface{} `json:"meta"` + } + var response ComponentSummaryResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to get component summary data, retmsg: %s", response.RetMsg) + log.Err(responseError) + return nil, responseError + } + // response.Data should be like: + //{ + // "intersect_num": 551, + // "intersect_rate": 1, + //} + return response.Data, nil +} + +// GetComponentOutputDataDownloadRequest returns a *http.Request object to be used to download the component data +func (c *client) GetComponentOutputDataDownloadRequest(request ComponentTrackingCommonRequest) (*http.Request, error) { + payload, err := json.Marshal(request) + if err != nil { + return nil, err + } + req, _ := http.NewRequest("GET", c.genURL("tracking/component/output/data/download"), bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + return req, nil +} + +// ConvertHomoModel calls the model/homo/convert API to convert the model +func (c *client) ConvertHomoModel(request HomoModelConversionRequest) error { + resp, err := c.postJSON("model/homo/convert", request) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return err + } + var response CommonResponse + if err := json.Unmarshal(body, &response); err != nil { + return err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to convert homo model, retmsg: %s", response.RetMsg) + log.Err(responseError) + return responseError + } + return nil +} + +// DeployHomoModel calls the FATE-Flow API to deploy the model +func (c *client) DeployHomoModel(request HomoModelDeploymentRequest) (string, error) { + resp, err := c.postJSON("model/homo/deploy", request) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return "", err + } + var response CommonResponse + if err := json.Unmarshal(body, &response); err != nil { + return string(body), err + } + if response.RetCode != 0 { + responseError := errors.Errorf("failed to deploy homo model, retmsg: %s", response.RetMsg) + log.Err(responseError) + return string(body), responseError + } + return string(body), nil +} + +func (c *client) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return nil, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + return nil, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} + +func (c *client) postJSON(path string, body interface{}) (*http.Response, error) { + url := c.genURL(path) + var payload []byte + if stringBody, ok := body.(string); ok { + payload = []byte(stringBody) + } else { + var err error + if payload, err = json.Marshal(body); err != nil { + return nil, err + } + } + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + log.Info().Msg(fmt.Sprintf("Posting request to %s with body %s", url, string(payload))) + return http.DefaultClient.Do(req) +} + +func (c *client) genURL(path string) string { + schemaStr := "http" + if c.https { + schemaStr = "https" + } + return fmt.Sprintf("%s://%s:%d/v1/%s", schemaStr, c.host, c.port, path) +} diff --git a/site-portal/server/infrastructure/fateclient/template/general.go b/site-portal/server/infrastructure/fateclient/template/general.go new file mode 100644 index 00000000..35cbad56 --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/general.go @@ -0,0 +1,103 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog/log" + "strconv" +) + +const trainingGeneralConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ], + "arbiter": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + } +}` + +// GeneralTrainingParam contains common parameters for creating all kins of jobs +type GeneralTrainingParam struct { + Guest PartyDataInfo + Hosts []PartyDataInfo +} + +// BuildTrainingConfGeneralStr returns a json string, which is a part of the final FATE job conf file. Including +// "dsl_version", "initiator", "role" and "job_parameters". +func BuildTrainingConfGeneralStr(param GeneralTrainingParam) (string, error) { + hostNum := len(param.Hosts) + if param.Hosts == nil || hostNum == 0 { + log.Debug().Msg("Build training conf for a guest only training job") + return fmt.Sprintf(trainingGeneralConf, param.Guest.PartyID, param.Guest.PartyID, "", + param.Guest.PartyID), nil + } + hostArrayStr := param.Hosts[0].PartyID + for i := 1; i < hostNum; i++ { + hostArrayStr += ", " + param.Hosts[i].PartyID + } + return fmt.Sprintf(trainingGeneralConf, param.Guest.PartyID, param.Guest.PartyID, hostArrayStr, + param.Hosts[0].PartyID), nil +} + +// buildHostParams is a helper function which can help generate the parameter string for the hosts +func buildHostParams(hostInfos []PartyDataInfo, template string) (string, string, error) { + hostArrayStr := "" + hostParamMap := make(map[string]interface{}) + for index, host := range hostInfos { + if hostArrayStr == "" { + hostArrayStr = host.PartyID + } else { + hostArrayStr += ", " + host.PartyID + } + indexStr := strconv.Itoa(index) + readerConfStr := fmt.Sprintf(template, host.TableName, host.TableNamespace) + var readerConf map[string]interface{} + if err := json.Unmarshal([]byte(readerConfStr), &readerConf); err != nil { + return "", "", err + } + hostParamMap[indexStr] = readerConf + } + hostParamBytes, err := json.Marshal(hostParamMap) + if err != nil { + return "", "", err + } + hostParamStr := string(hostParamBytes) + return hostParamStr, hostArrayStr, err +} diff --git a/site-portal/server/infrastructure/fateclient/template/homo.go b/site-portal/server/infrastructure/fateclient/template/homo.go new file mode 100644 index 00000000..f32279cb --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/homo.go @@ -0,0 +1,189 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "bytes" + "encoding/json" + "fmt" +) + +const homoHostComponentParamTemplate = ` +{ + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } +} +` + +const homoPredictingJobConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "%s", + "party_id": %s + }, + "role": { + "%s": [ + %s + ] + }, + "job_parameters": { + "common": { + "work_mode": 1, + "backend": 2, + "job_type": "predict", + "model_id": "%s", + "model_version": "%s" + } + }, + "component_parameters": { + "role": { + "%s": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` + +// HomoTrainingParam contains parameters for a horizontal job +type HomoTrainingParam struct { + Guest PartyDataInfo + Hosts []PartyDataInfo + LabelName string + ValidationEnabled bool + ValidationPercent uint + Type HomoAlgorithmType +} + +// HomoPredictingParam contains parameters for creating a predicting job for a horizontal model +type HomoPredictingParam struct { + Role string + ModelID string + ModelVersion string + PartyDataInfo PartyDataInfo +} + +// HomoAlgorithmType is the enum of horizontal algorithm types +type HomoAlgorithmType uint8 + +const ( + HomoAlgorithmTypeUnknown HomoAlgorithmType = iota + HomoAlgorithmTypeLR + HomoAlgorithmTypeSBT +) + +var homoAlgorithmTypeTemplateMap = map[HomoAlgorithmType]map[bool][]string{ + HomoAlgorithmTypeLR: { + true: { + homoLRHomoDataSplitDSL, + homoLRHomoDataSplitConf, + }, + false: { + homoLRDSL, + homoLRConf, + }, + }, + HomoAlgorithmTypeSBT: { + true: { + homoSBTHomoDataSplitDSL, + homoSBTHomoDataSplitConf, + }, + false: { + homoSBTDSL, + homoSBTConf, + }, + }, +} + +// BuildHomoTrainingConf returns the FATE job conf and dsl from the specified param +func BuildHomoTrainingConf(param HomoTrainingParam) (string, string, error) { + if param.LabelName == "" { + param.LabelName = "y" + } + hostParamStr, hostArrayStr, err := buildHostParams(param.Hosts, homoHostComponentParamTemplate) + if err != nil { + return "", "", err + } + dslStr := homoAlgorithmTypeTemplateMap[param.Type][param.ValidationEnabled][0] + confStr := homoAlgorithmTypeTemplateMap[param.Type][param.ValidationEnabled][1] + + arbiterPartyID := param.Guest.PartyID + if len(param.Hosts) > 0 { + arbiterPartyID = param.Hosts[0].PartyID + } + + if param.ValidationEnabled { + validationSizeStr := fmt.Sprintf("%0.2f", float64(param.ValidationPercent)/100) + confStr = fmt.Sprintf(confStr, + param.Guest.PartyID, + param.Guest.PartyID, + hostArrayStr, + arbiterPartyID, + validationSizeStr, + validationSizeStr, + param.LabelName, + hostParamStr, + param.Guest.TableName, + param.Guest.TableNamespace) + } else { + confStr = fmt.Sprintf(confStr, + param.Guest.PartyID, + param.Guest.PartyID, + hostArrayStr, + arbiterPartyID, + param.LabelName, + hostParamStr, + param.Guest.TableName, + param.Guest.TableNamespace) + } + var prettyJson bytes.Buffer + if err := json.Indent(&prettyJson, []byte(confStr), "", " "); err != nil { + return "", "", err + } + confStr = prettyJson.String() + + prettyJson.Reset() + if err := json.Indent(&prettyJson, []byte(dslStr), "", " "); err != nil { + return "", "", err + } + dslStr = prettyJson.String() + return confStr, dslStr, nil +} + +// BuildHomoPredictingConf returns the FATE job conf and dsl from the specified param +func BuildHomoPredictingConf(param HomoPredictingParam) (string, string, error) { + return fmt.Sprintf(homoPredictingJobConf, + param.Role, + param.PartyDataInfo.PartyID, + param.Role, + param.PartyDataInfo.PartyID, + param.ModelID, + param.ModelVersion, + param.Role, + param.PartyDataInfo.TableName, + param.PartyDataInfo.TableNamespace), "{}", nil +} diff --git a/site-portal/server/infrastructure/fateclient/template/homo_lr.go b/site-portal/server/infrastructure/fateclient/template/homo_lr.go new file mode 100644 index 00000000..92c555fc --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/homo_lr.go @@ -0,0 +1,343 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +const homoLRDSL = ` +{ + "components": { + "reader_0": { + "module": "Reader", + "output": { + "data": [ + "data" + ] + } + }, + "dataio_0": { + "module": "DataIO", + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "HomoLR_0": { + "module": "HomoLR", + "input": { + "data": { + "train_data": [ + "dataio_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + } + }, + "evaluation_0": { + "module": "Evaluation", + "input": { + "data": { + "data": [ + "HomoLR_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ] + } + } + } +} +` + +const homoLRConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ], + "arbiter": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "use_encrypt": false, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + }, + "component_parameters": { + "common": { + "dataio_0": { + "with_label": true, + "output_format": "dense", + "label_type": "int", + "label_name": "%s" + }, + "HomoLR_0": { + "penalty": "L2", + "tol": 0.00001, + "alpha": 0.01, + "optimizer": "sgd", + "batch_size": -1, + "learning_rate": 0.15, + "init_param": { + "init_method": "zeros" + }, + "max_iter": 30, + "early_stop": "diff", + "encrypt_param": { + "method": null + }, + "cv_param": { + "n_splits": 4, + "shuffle": true, + "random_seed": 33, + "need_cv": false + }, + "decay": 1, + "decay_sqrt": true + }, + "evaluation_0": { + "eval_type": "binary" + } + }, + "role": { + "host": %s, + "guest": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` + +const homoLRHomoDataSplitDSL = ` +{ + "components": { + "homo_data_split_0": { + "output": { + "data": [ + "train_data", + "validate_data", + "test_data" + ] + }, + "input": { + "data": { + "data": [ + "dataio_0.data" + ] + } + }, + "module": "HomoDataSplit" + }, + "reader_0": { + "output": { + "data": [ + "data" + ] + }, + "module": "Reader" + }, + "evaluation_0": { + "output": { + "data": [ + "data" + ] + }, + "input": { + "data": { + "data": [ + "HomoLR_0.data" + ] + } + }, + "module": "Evaluation" + }, + "dataio_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "module": "DataIO" + }, + "HomoLR_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "validate_data": [ + "homo_data_split_0.validate_data" + ], + "train_data": [ + "homo_data_split_0.train_data" + ] + } + }, + "module": "HomoLR" + } + } +} +` + +const homoLRHomoDataSplitConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ], + "arbiter": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + }, + "component_parameters": { + "common": { + "homo_data_split_0": { + "validate_size": %s, + "split_points": [ + 0, + %s + ], + "test_size": 0, + "stratified": true + }, + "dataio_0": { + "with_label": true, + "output_format": "dense", + "label_type": "int", + "label_name": "%s" + }, + "HomoLR_0": { + "penalty": "L2", + "tol": 0.00001, + "alpha": 0.01, + "optimizer": "sgd", + "batch_size": -1, + "learning_rate": 0.15, + "init_param": { + "init_method": "zeros" + }, + "max_iter": 30, + "early_stop": "diff", + "encrypt_param": { + "method": null + }, + "cv_param": { + "n_splits": 4, + "shuffle": true, + "random_seed": 33, + "need_cv": false + }, + "decay": 1, + "decay_sqrt": true + }, + "evaluation_0": { + "eval_type": "binary" + } + }, + "role": { + "host": %s, + "guest": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` diff --git a/site-portal/server/infrastructure/fateclient/template/homo_sbt.go b/site-portal/server/infrastructure/fateclient/template/homo_sbt.go new file mode 100644 index 00000000..7cd6a763 --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/homo_sbt.go @@ -0,0 +1,316 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +const homoSBTDSL = ` +{ + "components": { + "reader_0": { + "output": { + "data": [ + "data" + ] + }, + "module": "Reader" + }, + "evaluation_0": { + "output": { + "data": [ + "data" + ] + }, + "input": { + "data": { + "data": [ + "HomoSecureboost_0.data" + ] + } + }, + "module": "Evaluation" + }, + "dataio_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "module": "DataIO" + }, + "HomoSecureboost_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "train_data": [ + "dataio_0.data" + ] + } + }, + "module": "HomoSecureboost" + } + } +} +` + +const homoSBTConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ], + "arbiter": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + }, + "component_parameters": { + "common": { + "evaluation_0": { + "eval_type": "binary" + }, + "HomoSecureboost_0": { + "task_type": "classification", + "objective_param": { + "objective": "cross_entropy" + }, + "num_trees": 3, + "validation_freqs": 1, + "tree_param": { + "max_depth": 3 + } + }, + "dataio_0": { + "with_label": true, + "output_format": "dense", + "label_type": "int", + "label_name": "%s" + } + }, + "role": { + "host": %s, + "guest": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` + +const homoSBTHomoDataSplitDSL = ` +{ + "components": { + "homo_data_split_0": { + "output": { + "data": [ + "train_data", + "validate_data", + "test_data" + ] + }, + "input": { + "data": { + "data": [ + "dataio_0.data" + ] + } + }, + "module": "HomoDataSplit" + }, + "reader_0": { + "output": { + "data": [ + "data" + ] + }, + "module": "Reader" + }, + "evaluation_0": { + "output": { + "data": [ + "data" + ] + }, + "input": { + "data": { + "data": [ + "HomoSecureboost_0.data" + ] + } + }, + "module": "Evaluation" + }, + "dataio_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "module": "DataIO" + }, + "HomoSecureboost_0": { + "output": { + "data": [ + "data" + ], + "model": [ + "model" + ] + }, + "input": { + "data": { + "validate_data": [ + "homo_data_split_0.validate_data" + ], + "train_data": [ + "homo_data_split_0.train_data" + ] + } + }, + "module": "HomoSecureboost" + } + } +} +` + +const homoSBTHomoDataSplitConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ], + "arbiter": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + }, + "component_parameters": { + "common": { + "homo_data_split_0": { + "validate_size": %s, + "split_points": [ + 0, + %s + ], + "test_size": 0, + "stratified": true + }, + "dataio_0": { + "with_label": true, + "output_format": "dense", + "label_type": "int", + "label_name": "%s" + }, + "HomoSecureboost_0": { + "task_type": "classification", + "objective_param": { + "objective": "cross_entropy" + }, + "num_trees": 3, + "validation_freqs": 1, + "tree_param": { + "max_depth": 3 + } + }, + "evaluation_0": { + "eval_type": "binary" + } + }, + "role": { + "host": %s, + "guest": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` diff --git a/site-portal/server/infrastructure/fateclient/template/homo_test.go b/site-portal/server/infrastructure/fateclient/template/homo_test.go new file mode 100644 index 00000000..dce16f4c --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/homo_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildHomoLRConf(t *testing.T) { + for _, algorithmType := range []HomoAlgorithmType{HomoAlgorithmTypeLR, HomoAlgorithmTypeSBT} { + info := HomoTrainingParam{ + Guest: PartyDataInfo{ + PartyID: "9999", + TableName: "tb1", + TableNamespace: "tbns1", + }, + Hosts: []PartyDataInfo{ + { + PartyID: "10000", + TableName: "tb2", + TableNamespace: "tbns2", + }, + }, + Type: algorithmType, + } + conf, dsl, err := BuildHomoTrainingConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + + info.Hosts = append(info.Hosts, PartyDataInfo{ + PartyID: "20000", + TableName: "tb3", + TableNamespace: "tbns3", + }) + conf, dsl, err = BuildHomoTrainingConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + + info.Hosts = nil + conf, dsl, err = BuildHomoTrainingConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + + info.ValidationEnabled = true + info.ValidationPercent = 80 + conf, dsl, err = BuildHomoTrainingConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + } +} diff --git a/site-portal/server/infrastructure/fateclient/template/party.go b/site-portal/server/infrastructure/fateclient/template/party.go new file mode 100644 index 00000000..d9430aaf --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/party.go @@ -0,0 +1,22 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +// PartyDataInfo is contains party info and its data info +type PartyDataInfo struct { + PartyID string + TableName string + TableNamespace string +} diff --git a/site-portal/server/infrastructure/fateclient/template/psi.go b/site-portal/server/infrastructure/fateclient/template/psi.go new file mode 100644 index 00000000..6d40295b --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/psi.go @@ -0,0 +1,161 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "fmt" +) + +const PSIDSL = ` +{ + "components": { + "reader_0": { + "module": "Reader", + "output": { + "data": [ + "data" + ] + } + }, + "dataio_0": { + "module": "DataIO", + "input": { + "data": { + "data": [ + "reader_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ] + } + }, + "intersection_0": { + "module": "Intersection", + "input": { + "data": { + "data": [ + "dataio_0.data" + ] + } + }, + "output": { + "data": [ + "data" + ] + } + } + } +} +` + +const PSIConf = ` +{ + "dsl_version": 2, + "initiator": { + "role": "guest", + "party_id": %s + }, + "role": { + "guest": [ + %s + ], + "host": [ + %s + ] + }, + "job_parameters": { + "common": { + "job_type": "train", + "backend": 2, + "work_mode": 1, + "spark_run": { + "num-executors": 1, + "executor-cores": 1, + "total-executor-cores": 1 + } + } + }, + "component_parameters": { + "common": { + "intersect_0": { + "intersect_method": "rsa", + "sync_intersect_ids": false, + "only_output_key": true, + "rsa_params": { + "hash_method": "sha256", + "final_hash_method": "sha256", + "split_calculation": false, + "key_length": 2048 + } + }, + "dataio_0": { + "with_label": false, + "output_format": "dense", + "label_type": "int" + } + }, + "role": { + "host": %s, + "guest": { + "0": { + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } + } + } + } + } +} +` + +const PSIHostParamTemplate = ` +{ + "reader_0": { + "table": { + "name": "%s", + "namespace": "%s" + } + } +} +` + +// PSIParam contains parameters for a PSI job +type PSIParam struct { + Guest PartyDataInfo + Hosts []PartyDataInfo +} + +// BuildPsiConf returns the DSL and Conf files +func BuildPsiConf(param PSIParam) (string, string, error) { + hostParamStr, hostArrayStr, err := buildHostParams(param.Hosts, PSIHostParamTemplate) + if err != nil { + return "", "", err + } + confStr := fmt.Sprintf(PSIConf, + param.Guest.PartyID, + param.Guest.PartyID, + hostArrayStr, + hostParamStr, + param.Guest.TableName, + param.Guest.TableNamespace, + ) + return confStr, PSIDSL, nil +} diff --git a/site-portal/server/infrastructure/fateclient/template/psi_test.go b/site-portal/server/infrastructure/fateclient/template/psi_test.go new file mode 100644 index 00000000..da409fd2 --- /dev/null +++ b/site-portal/server/infrastructure/fateclient/template/psi_test.go @@ -0,0 +1,58 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package template + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBuildPsiConf(t *testing.T) { + info := PSIParam{ + Guest: PartyDataInfo{ + PartyID: "9999", + TableName: "tb1", + TableNamespace: "tbns1", + }, + Hosts: []PartyDataInfo{ + { + PartyID: "10000", + TableName: "tb2", + TableNamespace: "tbns2", + }, + }, + } + conf, dsl, err := BuildPsiConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + + info.Hosts = append(info.Hosts, PartyDataInfo{ + PartyID: "20000", + TableName: "tb3", + TableNamespace: "tbns3", + }) + conf, dsl, err = BuildPsiConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) + + info.Hosts = nil + conf, dsl, err = BuildPsiConf(info) + assert.NoError(t, err) + assert.True(t, json.Valid([]byte(conf))) + assert.True(t, json.Valid([]byte(dsl))) +} diff --git a/site-portal/server/infrastructure/fmlmanager/client.go b/site-portal/server/infrastructure/fmlmanager/client.go new file mode 100644 index 00000000..cde1a78c --- /dev/null +++ b/site-portal/server/infrastructure/fmlmanager/client.go @@ -0,0 +1,411 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fmlmanager + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +type client struct { + // endpoint address + endpoint string + serverName string +} + +// NewFMLManagerClient returns a client to FML manager service +func NewFMLManagerClient(endpoint string, serverName string) *client { + return &client{ + endpoint: endpoint, + serverName: serverName, + } +} + +// CreateSite registered a site to FML manager +func (c *client) CreateSite(site *Site) error { + resp, err := c.postJSON("site", site) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendProjectClosing sends a project closing request +func (c *client) SendProjectClosing(projectUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/%s/close", projectUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendInvitation sends an invitation request +func (c *client) SendInvitation(invitation ProjectInvitation) error { + resp, err := c.postJSON("project/invitation", invitation) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendInvitationAcceptance sends invitation acceptance response +func (c *client) SendInvitationAcceptance(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/invitation/%s/accept", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendInvitationRejection sends invitation reject response +func (c *client) SendInvitationRejection(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/invitation/%s/reject", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendInvitationRevocation send invitation revocation request +func (c *client) SendInvitationRevocation(invitationUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/invitation/%s/revoke", invitationUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendProjectDataAssociation sends new project data association to FML manager +func (c *client) SendProjectDataAssociation(projectUUID string, association ProjectDataAssociation) error { + resp, err := c.postJSON(fmt.Sprintf("project/%s/data/associate", projectUUID), association) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendProjectDataDismissal sends project data dismissal to FML manager +func (c *client) SendProjectDataDismissal(projectUUID string, association ProjectDataAssociationBase) error { + resp, err := c.postJSON(fmt.Sprintf("project/%s/data/dismiss", projectUUID), association) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendProjectParticipantLeaving sends project participant leaving to FML manager +func (c *client) SendProjectParticipantLeaving(projectUUID, siteUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/%s/participant/%s/leave", projectUUID, siteUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendProjectParticipantDismissal sends project participant dismissal to FML manager +func (c *client) SendProjectParticipantDismissal(projectUUID, siteUUID string) error { + resp, err := c.postJSON(fmt.Sprintf("project/%s/participant/%s/dismiss", projectUUID, siteUUID), "") + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendJobCreationRequest sends job creation request +func (c *client) SendJobCreationRequest(uuid, username string, originalRequest string) error { + creationRequest := &JobRemoteJobCreationRequest{} + if err := json.Unmarshal([]byte(originalRequest), creationRequest); err != nil { + return err + } + creationRequest.UUID = uuid + creationRequest.Username = username + resp, err := c.postJSON("job/create", creationRequest) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendJobApprovalResponse sends job approval response +func (c *client) SendJobApprovalResponse(jobUUID string, approvalContext JobApprovalContext) error { + resp, err := c.postJSON(fmt.Sprintf("job/%s/response", jobUUID), approvalContext) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// SendJobStatusUpdate sends job status update +func (c *client) SendJobStatusUpdate(jobUUID string, context JobStatusUpdateContext) error { + resp, err := c.postJSON(fmt.Sprintf("job/%s/status", jobUUID), context) + if err != nil { + return err + } + defer resp.Body.Close() + _, err = c.parseResponse(resp) + return err +} + +// GetAllSite gets all sites from the FML manager +func (c *client) GetAllSite() ([]Site, error) { + urlStr := c.genURL("site") + var resp *http.Response + u, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Wrap(err, "Parse URL failed") + } + if u.Scheme == "https" { + client, err := c.getHttpsClientWithCert() + if err != nil { + return nil, err + } + resp, err = client.Get(urlStr) + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTPs", urlStr)) + } else { + resp, err = http.Get(urlStr) + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTP", urlStr)) + } + defer resp.Body.Close() + body, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type GetSiteResponse struct { + CommonResponse + Sites []Site `json:"data"` + } + var getSiteResponse GetSiteResponse + if err := json.Unmarshal(body, &getSiteResponse); err != nil { + return nil, err + } + return getSiteResponse.Sites, nil +} + +// GetProjectDataAssociation returns a map of associated data items of a project, indexed by data uuid +func (c *client) GetProjectDataAssociation(projectUUID string) (map[string]ProjectDataAssociation, error) { + resp, err := c.getJSON(fmt.Sprintf("project/%s/data", projectUUID), nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + responseByte, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type Response struct { + CommonResponse + Data map[string]ProjectDataAssociation `json:"data"` + } + var getAssociationResponse Response + if err := json.Unmarshal(responseByte, &getAssociationResponse); err != nil { + return nil, err + } + return getAssociationResponse.Data, nil +} + +// GetProjectParticipant returns a map of participant items of a project, indexed by participant uuid +func (c *client) GetProjectParticipant(projectUUID string) (map[string]ProjectParticipant, error) { + resp, err := c.getJSON(fmt.Sprintf("project/%s/participant", projectUUID), nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + responseByte, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type Response struct { + CommonResponse + Data map[string]ProjectParticipant `json:"data"` + } + var getParticipantResponse Response + if err := json.Unmarshal(responseByte, &getParticipantResponse); err != nil { + return nil, err + } + return getParticipantResponse.Data, nil +} + +// GetProject returns a list of projects the current site is related to +func (c *client) GetProject(siteUUID string) (map[string]ProjectInfoWithStatus, error) { + resp, err := c.getJSON("project", map[string]string{"participant": siteUUID}) + if err != nil { + return nil, err + } + defer resp.Body.Close() + responseByte, err := c.parseResponse(resp) + if err != nil { + return nil, err + } + type Response struct { + CommonResponse + Data map[string]ProjectInfoWithStatus `json:"data"` + } + var getProjectResponse Response + if err := json.Unmarshal(responseByte, &getProjectResponse); err != nil { + return nil, err + } + return getProjectResponse.Data, nil +} + +func (c *client) getJSON(path string, query map[string]string) (*http.Response, error) { + urlStr := c.genURL(path) + baseUrl, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + params := url.Values{} + for k, v := range query { + params.Add(k, v) + } + baseUrl.RawQuery = params.Encode() + + urlStr = baseUrl.String() + var resp *http.Response + if baseUrl.Scheme == "https" { + client, err := c.getHttpsClientWithCert() + if err != nil { + return nil, err + } + resp, err = client.Get(urlStr) + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTPs", urlStr)) + } else { + resp, err = http.Get(urlStr) + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Getting %s via HTTP", urlStr)) + } + return resp, nil +} + +func (c *client) postJSON(path string, body interface{}) (*http.Response, error) { + urlStr := c.genURL(path) + var payload []byte + if stringBody, ok := body.(string); ok { + payload = []byte(stringBody) + } else { + var err error + if payload, err = json.Marshal(body); err != nil { + return nil, err + } + } + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + u, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Wrap(err, "Parse URL failed") + } + //if endpoint scheme is https, use tls + if u.Scheme == "https" { + client, err := c.getHttpsClientWithCert() + if err != nil { + return nil, err + } + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTPs with body %s", urlStr, string(payload))) + return client.Do(req) + } else { + log.Info().Msg(fmt.Sprintf("Posting request to %s via HTTP with body %s", urlStr, string(payload))) + return http.DefaultClient.Do(req) + } +} + +func (c *client) parseResponse(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error().Err(err).Msg("read response body error") + return nil, err + } + log.Info().Str("body", string(body)).Msg("response body") + if response.StatusCode != http.StatusOK { + log.Error().Msgf("request error: %s", response.Status) + return nil, errors.Errorf("request error: %s, body: %s", response.Status, string(body)) + } + return body, nil +} + +func (c *client) genURL(path string) string { + return fmt.Sprintf("%s/api/v1/%s", c.endpoint, path) +} + +// getHttpsClientWithCert returns a https client use siteportal client cert +func (c *client) getHttpsClientWithCert() (*http.Client, error) { + caCertPath := viper.GetString("siteportal.tls.ca.cert") + sitePortalClientCert := viper.GetString("siteportal.tls.client.cert") + sitePortalClientKey := viper.GetString("siteportal.tls.client.key") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + return nil, errors.Wrapf(err, "read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + clientCrt, err := tls.LoadX509KeyPair(sitePortalClientCert, sitePortalClientKey) + if err != nil { + return nil, errors.Wrapf(err, "LoadX509KeyPair error:") + } + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{clientCrt}, + ServerName: c.serverName, + }, + } + return &http.Client{Transport: tr}, nil +} diff --git a/site-portal/server/infrastructure/fmlmanager/types.go b/site-portal/server/infrastructure/fmlmanager/types.go new file mode 100644 index 00000000..ab4058c4 --- /dev/null +++ b/site-portal/server/infrastructure/fmlmanager/types.go @@ -0,0 +1,153 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fmlmanager + +import ( + "time" +) + +// CommonResponse is the structure of all FML manager response +type CommonResponse struct { + Code int `json:"code" example:"0"` + Message string `json:"message" example:"success"` + Data interface{} `json:"data" swaggertype:"object"` +} + +// Site contains all the info for the current site +type Site struct { + UUID string `json:"uuid"` + // Name is the user's name + Name string `json:"name"` + // Description contains more text about this site + Description string `json:"description"` + // PartyID is the id of this party + PartyID uint `json:"party_id"` + // ExternalHost is the IP or hostname this site portal service is exposed + ExternalHost string `json:"external_host"` + // ExternalPort the port number this site portal service is exposed + ExternalPort uint `json:"external_port"` + // HTTPS choose if site portal enable HTTPS, 'true' use HTTPS, 'false'use HTTPS + HTTPS bool `json:"https"` + // ServerName is used by FML Manager to verify site portal's certificate when HTTPs is enabled + ServerName string `json:"server_name"` +} + +// ProjectInvitation is the invitation we send to FML manager for inviting a site +// We send targeting site uuid as well as project info and data association info +// so it can be created in the FML manager +type ProjectInvitation struct { + UUID string `json:"uuid"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` + AssociatedData []ProjectDataAssociation `json:"associated_data"` +} + +// ProjectDataAssociationBase contains the basic info of an association +type ProjectDataAssociationBase struct { + DataUUID string `json:"data_uuid"` +} + +// ProjectDataAssociation contains detailed info of an association +type ProjectDataAssociation struct { + ProjectDataAssociationBase + Name string `json:"name"` + Description string `json:"description"` + SiteName string `json:"site_name"` + SiteUUID string `json:"site_uuid"` + SitePartyID uint `json:"site_party_id"` + TableName string `json:"table_name"` + TableNamespace string `json:"table_namespace"` + CreationTime time.Time `json:"creation_time"` + UpdateTime time.Time `json:"update_time"` +} + +// ProjectParticipant is a site in a project +type ProjectParticipant struct { + UUID string `json:"uuid"` + ProjectUUID string `json:"project_uuid"` + SiteUUID string `json:"site_uuid"` + SiteName string `json:"site_name"` + SitePartyID uint `json:"site_party_id"` + SiteDescription string `json:"site_description"` + Status uint `json:"status"` +} + +// ProjectInfoWithStatus contains project basic information and the status inferred for certain participant +type ProjectInfoWithStatus struct { + ProjectUUID string `json:"project_uuid"` + ProjectName string `json:"project_name"` + ProjectDescription string `json:"project_description"` + ProjectAutoApprovalEnabled bool `json:"project_auto_approval_enabled"` + ProjectManager string `json:"project_manager"` + ProjectManagingSiteName string `json:"project_managing_site_name"` + ProjectManagingSitePartyID uint `json:"project_managing_site_party_id"` + ProjectManagingSiteUUID string `json:"project_managing_site_uuid"` + ProjectCreationTime time.Time `json:"project_creation_time"` + ProjectStatus uint `json:"project_status"` +} + +// JobDataBase describes one data configuration for a job +type JobDataBase struct { + DataUUID string `json:"data_uuid"` + LabelName string `json:"label_name"` +} + +// JobRemoteJobCreationRequest is the structure containing necessary info to create a job +type JobRemoteJobCreationRequest struct { + UUID string `json:"uuid"` + ConfJson string `json:"conf_json"` + DSLJson string `json:"dsl_json"` + Name string `json:"name"` + Description string `json:"description"` + Type uint8 `json:"type"` + ProjectUUID string `json:"project_uuid"` + InitiatorData JobDataBase `json:"initiator_data"` + OtherData []JobDataBase `json:"other_site_data"` + ValidationEnabled bool `json:"training_validation_enabled"` + ValidationSizePercent uint `json:"training_validation_percent"` + ModelName string `json:"training_model_name"` + AlgorithmType uint8 `json:"training_algorithm_type"` + AlgorithmComponentName string `json:"algorithm_component_name"` + EvaluateComponentName string `json:"evaluate_component_name"` + ComponentsToDeploy []string `json:"training_component_list_to_deploy"` + ModelUUID string `json:"predicting_model_uuid"` + Username string `json:"username"` +} + +// JobApprovalContext contains the issuing site and the approval result +type JobApprovalContext struct { + SiteUUID string `json:"site_uuid"` + Approved bool `json:"approved"` +} + +// JobStatusUpdateContext contains info of the updated job status +type JobStatusUpdateContext struct { + Status uint8 `json:"status"` + StatusMessage string `json:"status_message"` + FATEJobID string `json:"fate_job_id"` + FATEJobStatus string `json:"fate_job_status"` + FATEModelID string `json:"fate_model_id"` + FATEModelVersion string `json:"fate_model_version"` + ParticipantStatusMap map[string]uint8 `json:"participant_status_map"` +} diff --git a/site-portal/server/infrastructure/gorm/database.go b/site-portal/server/infrastructure/gorm/database.go new file mode 100644 index 00000000..9162f96e --- /dev/null +++ b/site-portal/server/infrastructure/gorm/database.go @@ -0,0 +1,73 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/spf13/viper" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +const ( + dsnTemplate = "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=Asia/Shanghai" +) + +// InitDB initialize the connection to the PostgresSQL db +func InitDB() error { + host := viper.GetString("postgres.host") + port := viper.GetString("postgres.port") + dbName := viper.GetString("postgres.db") + user := viper.GetString("postgres.user") + password := viper.GetString("postgres.password") + if host == "" || port == "" || dbName == "" || user == "" || password == "" { + panic("database information incomplete") + } + + sslMode := viper.GetString("postgres.sslmode") + if sslMode == "" { + sslMode = "disable" + } + + debugLog, _ := strconv.ParseBool(viper.GetString("postgres.debug")) + loglevel := logger.Silent + if debugLog { + loglevel = logger.Info + } + + dsn := fmt.Sprintf(dsnTemplate, host, port, user, password, dbName, sslMode) + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: loglevel, + Colorful: false, + }, + ) + if _db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: newLogger}); err != nil { + return err + } else { + db = _db + } + return nil +} diff --git a/site-portal/server/infrastructure/gorm/job_participant_repo.go b/site-portal/server/infrastructure/gorm/job_participant_repo.go new file mode 100644 index 00000000..834810bd --- /dev/null +++ b/site-portal/server/infrastructure/gorm/job_participant_repo.go @@ -0,0 +1,67 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// JobParticipantRepo implements repo.JobParticipantRepository using gorm and PostgreSQL +type JobParticipantRepo struct{} + +// make sure JobParticipantRepo implements the repo.JobParticipantRepository interface +var _ repo.JobParticipantRepository = (*JobParticipantRepo)(nil) + +func (r *JobParticipantRepo) Create(instance interface{}) error { + newJobParticipant := instance.(*entity.JobParticipant) + return db.Model(&entity.JobParticipant{}).Create(newJobParticipant).Error +} + +func (r *JobParticipantRepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.JobParticipant) + return db.Model(&entity.JobParticipant{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *JobParticipantRepo) GetByJobAndSiteUUID(jobUUID, siteUUID string) (interface{}, error) { + participant := &entity.JobParticipant{} + if err := db.Model(&entity.JobParticipant{}). + Where("job_uuid = ? AND site_uuid = ?", jobUUID, siteUUID). + First(participant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrJobParticipantNotFound + } + return nil, err + } + return participant, nil +} + +func (r *JobParticipantRepo) GetListByJobUUID(jobUUID string) (interface{}, error) { + var participantList []entity.JobParticipant + if err := db.Where("job_uuid = ?", jobUUID).Find(&participantList).Error; err != nil { + return nil, err + } + return participantList, nil +} + +// InitTable make sure the table is created in the db +func (r *JobParticipantRepo) InitTable() { + if err := db.AutoMigrate(&entity.JobParticipant{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/job_repo.go b/site-portal/server/infrastructure/gorm/job_repo.go new file mode 100644 index 00000000..7081a128 --- /dev/null +++ b/site-portal/server/infrastructure/gorm/job_repo.go @@ -0,0 +1,126 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// JobRepo implements repo.JobRepository using gorm and PostgreSQL +type JobRepo struct{} + +// make sure JobRepo implements the repo.JobRepository interface +var _ repo.JobRepository = (*JobRepo)(nil) + +var ErrJobNameConflict = errors.New("job name conflicts") + +func (r *JobRepo) Create(instance interface{}) error { + newJob := instance.(*entity.Job) + // TODO: check name conflicts? + // Add records + return db.Model(&entity.Job{}).Create(newJob).Error +} + +func (r *JobRepo) UpdateFATEJobInfoByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Select("fate_job_id", "fate_job_status", "fate_model_id", "fate_model_version").Updates(job).Error +} + +func (r *JobRepo) UpdateFATEJobStatusByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("fate_job_status", job.FATEJobStatus).Error +} + +func (r *JobRepo) UpdateStatusByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("status", job.Status).Error +} + +func (r *JobRepo) UpdateStatusMessageByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("status_message", job.StatusMessage).Error +} + +func (r *JobRepo) UpdateFinishTimeByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("finished_at", job.FinishedAt).Error +} + +func (r *JobRepo) UpdateResultInfoByUUID(instance interface{}) error { + job := instance.(*entity.Job) + return db.Model(&entity.Job{}).Where("uuid = ?", job.UUID). + Update("result_json", job.ResultJson).Error +} + +func (r *JobRepo) CheckNameConflict(name string) error { + var count int64 + err := db.Model(&entity.Job{}). + Where("name = ?", name). + Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return ErrJobNameConflict + } + return nil +} + +func (r *JobRepo) DeleteByProjectUUID(projectUUID string) error { + return db.Unscoped().Where("project_uuid = ?", projectUUID).Delete(&entity.Job{}).Error +} + +func (r *JobRepo) GetAll() (interface{}, error) { + var jobList []entity.Job + if err := db.Find(&jobList).Error; err != nil { + return nil, err + } + return jobList, nil +} + +func (r *JobRepo) GetListByProjectUUID(projectUUID string) (interface{}, error) { + var jobList []entity.Job + err := db.Where("project_uuid = ?", projectUUID).Find(&jobList).Error + if err != nil { + return 0, err + } + return jobList, nil +} + +func (r *JobRepo) GetByUUID(uuid string) (interface{}, error) { + job := &entity.Job{} + if err := db.Where("uuid = ?", uuid).First(&job).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrJobNotFound + } + return nil, err + } + return job, nil +} + +// InitTable make sure the table is created in the db +func (r *JobRepo) InitTable() { + if err := db.AutoMigrate(&entity.Job{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/local_data_repo.go b/site-portal/server/infrastructure/gorm/local_data_repo.go new file mode 100644 index 00000000..9542fdcb --- /dev/null +++ b/site-portal/server/infrastructure/gorm/local_data_repo.go @@ -0,0 +1,95 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" +) + +// LocalDataRepo implements repo.LocalDataRepository using gorm and PostgreSQL +type LocalDataRepo struct{} + +// make sure LocalDataRepo implements the repo.LocalDataRepository interface +var _ repo.LocalDataRepository = (*LocalDataRepo)(nil) + +// ErrLocalDataNameConflict means data with same name exists +var ErrLocalDataNameConflict = errors.New("data name cannot be the same with existing one") + +func (r *LocalDataRepo) Create(instance interface{}) error { + newData := instance.(*entity.LocalData) + if err := r.CheckNameConflict(newData.Name); err != nil { + return err + } + + if err := db.Model(&entity.LocalData{}).Create(newData).Error; err != nil { + return err + } + return nil +} + +func (r *LocalDataRepo) UpdateJobInfoByUUID(instance interface{}) error { + localData := instance.(*entity.LocalData) + return db.Model(localData).Where("uuid = ?", localData.UUID). + Select("job_id", "job_conf", "job_status", "job_error_msg"). + Updates(localData).Error +} + +func (r *LocalDataRepo) GetAll() (interface{}, error) { + var localDataList []entity.LocalData + if err := db.Find(&localDataList).Error; err != nil { + return nil, err + } + return localDataList, nil +} + +func (r *LocalDataRepo) GetByUUID(uuid string) (interface{}, error) { + localData := &entity.LocalData{} + if err := db.Model(&entity.LocalData{}).Where("uuid = ?", uuid).First(&localData).Error; err != nil { + return nil, err + } + return localData, nil +} + +func (r *LocalDataRepo) DeleteByUUID(uuid string) error { + return db.Model(&entity.LocalData{}).Where("uuid = ?", uuid).Delete(&entity.LocalData{}).Error +} + +func (r *LocalDataRepo) UpdateIDMetaInfoByUUID(uuid string, instance interface{}) error { + metaInfo := instance.(*valueobject.IDMetaInfo) + return db.Model(&entity.LocalData{}).Where("uuid = ?", uuid). + Update("id_meta_info", metaInfo).Error +} + +func (r *LocalDataRepo) CheckNameConflict(name string) error { + var count int64 + err := db.Model(&entity.LocalData{}).Where("name = ?", name).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return ErrLocalDataNameConflict + } + return nil +} + +// InitTable make sure the table is created in the db +func (r *LocalDataRepo) InitTable() { + if err := db.AutoMigrate(&entity.LocalData{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/model_deployment_repo.go b/site-portal/server/infrastructure/gorm/model_deployment_repo.go new file mode 100644 index 00000000..0a1b78ab --- /dev/null +++ b/site-portal/server/infrastructure/gorm/model_deployment_repo.go @@ -0,0 +1,51 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" +) + +// ModelDeploymentRepo implements repo.ModelDeploymentRepository using gorm and PostgreSQL +type ModelDeploymentRepo struct{} + +// make sure ModelRepo implements the repo.ModelRepository interface +var _ repo.ModelDeploymentRepository = (*ModelDeploymentRepo)(nil) + +func (r *ModelDeploymentRepo) Create(instance interface{}) error { + newDeployment := instance.(*entity.ModelDeployment) + // Add records + return db.Model(&entity.ModelDeployment{}).Create(newDeployment).Error +} + +func (r *ModelDeploymentRepo) UpdateStatusByUUID(instance interface{}) error { + deployment := instance.(*entity.ModelDeployment) + return db.Model(&entity.ModelDeployment{}).Where("uuid = ?", deployment.UUID). + Update("status", deployment.Status).Error +} + +func (r *ModelDeploymentRepo) UpdateResultJsonByUUID(instance interface{}) error { + deployment := instance.(*entity.ModelDeployment) + return db.Model(&entity.Job{}).Where("uuid = ?", deployment.UUID). + Update("result_json", deployment.ResultJson).Error +} + +// InitTable make sure the table is created in the db +func (r *ModelDeploymentRepo) InitTable() { + if err := db.AutoMigrate(&entity.ModelDeployment{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/model_repo.go b/site-portal/server/infrastructure/gorm/model_repo.go new file mode 100644 index 00000000..6ffd271e --- /dev/null +++ b/site-portal/server/infrastructure/gorm/model_repo.go @@ -0,0 +1,74 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ModelRepo implements repo.ModelRepository using gorm and PostgreSQL +type ModelRepo struct{} + +// make sure ModelRepo implements the repo.ModelRepository interface +var _ repo.ModelRepository = (*ModelRepo)(nil) + +func (r *ModelRepo) Create(instance interface{}) error { + newModel := instance.(*entity.Model) + // XXX: check name conflicts? + // Add records + return db.Model(&entity.Model{}).Create(newModel).Error +} + +func (r *ModelRepo) GetAll() (interface{}, error) { + var modelList []entity.Model + if err := db.Find(&modelList).Error; err != nil { + return nil, err + } + return modelList, nil +} + +func (r *ModelRepo) DeleteByUUID(uuid string) error { + return db.Where("uuid = ?", uuid).Delete(&entity.Model{}).Error +} + +func (r *ModelRepo) GetListByProjectUUID(projectUUID string) (interface{}, error) { + var modelList []entity.Model + err := db.Where("project_uuid = ?", projectUUID).Find(&modelList).Error + if err != nil { + return 0, err + } + return modelList, nil +} + +func (r *ModelRepo) GetByUUID(uuid string) (interface{}, error) { + model := &entity.Model{} + if err := db.Where("uuid = ?", uuid).First(model).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrModelNotFound + } + return nil, err + } + return model, nil +} + +// InitTable make sure the table is created in the db +func (r *ModelRepo) InitTable() { + if err := db.AutoMigrate(&entity.Model{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/project_data_repo.go b/site-portal/server/infrastructure/gorm/project_data_repo.go new file mode 100644 index 00000000..1302da42 --- /dev/null +++ b/site-portal/server/infrastructure/gorm/project_data_repo.go @@ -0,0 +1,109 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectDataRepo is the implementation of repo.ProjectDataRepository +type ProjectDataRepo struct{} + +var _ repo.ProjectDataRepository = (*ProjectDataRepo)(nil) + +func (r *ProjectDataRepo) Create(instance interface{}) error { + newData := instance.(*entity.ProjectData) + // Add records + return db.Model(&entity.ProjectData{}).Create(newData).Error +} + +func (r *ProjectDataRepo) GetByProjectAndDataUUID(projectUUID string, dataUUID string) (interface{}, error) { + data := &entity.ProjectData{} + if err := db.Model(&entity.ProjectData{}). + Where("project_uuid = ? AND data_uuid = ?", projectUUID, dataUUID). + First(data).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectDataNotFound + } + return nil, err + } + return data, nil +} + +func (r *ProjectDataRepo) UpdateStatusByUUID(instance interface{}) error { + data := instance.(*entity.ProjectData) + return db.Model(&entity.ProjectData{}).Where("uuid = ?", data.UUID). + Update("status", data.Status).Error +} + +func (r *ProjectDataRepo) GetListByProjectUUID(projectUUID string) (interface{}, error) { + var projectDataList []entity.ProjectData + err := db.Where("project_uuid = ?", projectUUID).Find(&projectDataList).Error + if err != nil { + return 0, err + } + return projectDataList, nil +} + +func (r *ProjectDataRepo) GetListByProjectAndSiteUUID(projectUUID string, siteUUID string) (interface{}, error) { + var projectDataList []entity.ProjectData + err := db.Where("project_uuid = ? AND site_uuid = ?", projectUUID, siteUUID).Find(&projectDataList).Error + if err != nil { + return 0, err + } + return projectDataList, nil +} + +func (r *ProjectDataRepo) GetByDataUUID(dataUUID string) (interface{}, error) { + projectData := &entity.ProjectData{} + err := db.Where("data_uuid = ?", dataUUID).Last(&projectData).Error + if err != nil { + return 0, err + } + return projectData, nil +} + +func (r *ProjectDataRepo) GetListByDataUUID(dataUUID string) (interface{}, error) { + var projectDataList []entity.ProjectData + err := db.Where("data_uuid = ?", dataUUID).Find(&projectDataList).Error + if err != nil { + return 0, err + } + return projectDataList, nil +} + +func (r *ProjectDataRepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.ProjectData{}).Error +} + +func (r *ProjectDataRepo) DeleteByProjectUUID(projectUUID string) error { + return db.Unscoped().Where("project_uuid = ?", projectUUID).Delete(&entity.ProjectData{}).Error +} + +func (r *ProjectDataRepo) UpdateSiteInfoBySiteUUID(instance interface{}) error { + site := instance.(*entity.ProjectData) + return db.Where("site_uuid = ?", site.SiteUUID). + Select("site_name", "site_party_id").Updates(site).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectDataRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectData{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/project_invitation_repo.go b/site-portal/server/infrastructure/gorm/project_invitation_repo.go new file mode 100644 index 00000000..8ece9b72 --- /dev/null +++ b/site-portal/server/infrastructure/gorm/project_invitation_repo.go @@ -0,0 +1,72 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" +) + +// ProjectInvitationRepo is the implementation of repo.ProjectInvitationRepository +type ProjectInvitationRepo struct{} + +var _ repo.ProjectInvitationRepository = (*ProjectInvitationRepo)(nil) + +func (r *ProjectInvitationRepo) Create(instance interface{}) error { + newInvitation := instance.(*entity.ProjectInvitation) + // Add records + return db.Model(&entity.ProjectInvitation{}).Create(newInvitation).Error +} + +func (r *ProjectInvitationRepo) UpdateStatusByUUID(instance interface{}) error { + invitation := instance.(*entity.ProjectInvitation) + return db.Model(&entity.ProjectInvitation{}).Where("uuid = ?", invitation.UUID). + Update("status", invitation.Status).Error +} + +func (r *ProjectInvitationRepo) GetByProjectUUID(uuid string) (interface{}, error) { + invitation := &entity.ProjectInvitation{} + if err := db.Model(&entity.ProjectInvitation{}).Where("project_uuid = ?", uuid). + Last(invitation).Error; err != nil { + return nil, err + } + return invitation, nil +} + +func (r *ProjectInvitationRepo) GetByProjectAndSiteUUID(projectUUID, siteUUID string) (interface{}, error) { + invitation := &entity.ProjectInvitation{} + if err := db.Model(&entity.ProjectInvitation{}). + Where("project_uuid = ? AND site_uuid = ?", projectUUID, siteUUID). + Last(invitation).Error; err != nil { + return nil, err + } + return invitation, nil +} + +func (r *ProjectInvitationRepo) GetByUUID(uuid string) (interface{}, error) { + invitation := &entity.ProjectInvitation{} + if err := db.Model(&entity.ProjectInvitation{}).Where("uuid = ?", uuid). + First(invitation).Error; err != nil { + return nil, err + } + return invitation, nil +} + +// InitTable make sure the table is created in the db +func (r *ProjectInvitationRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectInvitation{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/project_participant_repo.go b/site-portal/server/infrastructure/gorm/project_participant_repo.go new file mode 100644 index 00000000..be87695f --- /dev/null +++ b/site-portal/server/infrastructure/gorm/project_participant_repo.go @@ -0,0 +1,89 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectParticipantRepo is the implementation of repo.ProjectParticipantRepository +type ProjectParticipantRepo struct{} + +var _ repo.ProjectParticipantRepository = (*ProjectParticipantRepo)(nil) + +func (r *ProjectParticipantRepo) CountJoinedParticipantByProjectUUID(uuid string) (int64, error) { + var count int64 + err := db.Model(&entity.ProjectParticipant{}). + Where("project_uuid = ? AND status = ?", uuid, entity.ProjectParticipantStatusJoined). + Count(&count).Error + if err != nil { + return 0, err + } + return count, nil +} + +func (r *ProjectParticipantRepo) GetByProjectUUID(uuid string) (interface{}, error) { + var participantList []entity.ProjectParticipant + err := db.Where("project_uuid = ?", uuid).Find(&participantList).Error + if err != nil { + return 0, err + } + return participantList, nil +} + +func (r *ProjectParticipantRepo) GetByProjectAndSiteUUID(projectUUID, siteUUID string) (interface{}, error) { + participant := &entity.ProjectParticipant{} + if err := db.Model(&entity.ProjectParticipant{}). + Where("project_uuid = ? AND site_uuid = ?", projectUUID, siteUUID). + First(participant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectParticipantNotFound + } + return nil, err + } + return participant, nil +} + +func (r *ProjectParticipantRepo) Create(instance interface{}) error { + newParticipant := instance.(*entity.ProjectParticipant) + // Add records + return db.Model(&entity.ProjectParticipant{}).Create(newParticipant).Error +} + +func (r *ProjectParticipantRepo) UpdateStatusByUUID(instance interface{}) error { + participant := instance.(*entity.ProjectParticipant) + return db.Model(&entity.ProjectParticipant{}).Where("uuid = ?", participant.UUID). + Update("status", participant.Status).Error +} + +func (r *ProjectParticipantRepo) DeleteByProjectUUID(projectUUID string) error { + return db.Unscoped().Where("project_uuid = ?", projectUUID).Delete(&entity.ProjectParticipant{}).Error +} + +func (r *ProjectParticipantRepo) UpdateParticipantInfoBySiteUUID(instance interface{}) error { + participant := instance.(*entity.ProjectParticipant) + return db.Model(&entity.ProjectParticipant{}).Where("site_uuid = ?", participant.SiteUUID). + Select("site_name", "site_party_id", "site_description").Updates(participant).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectParticipantRepo) InitTable() { + if err := db.AutoMigrate(&entity.ProjectParticipant{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/project_repo.go b/site-portal/server/infrastructure/gorm/project_repo.go new file mode 100644 index 00000000..42b5850e --- /dev/null +++ b/site-portal/server/infrastructure/gorm/project_repo.go @@ -0,0 +1,104 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// ProjectRepo is the implementation of repo.ProjectRepository +type ProjectRepo struct{} + +var _ repo.ProjectRepository = (*ProjectRepo)(nil) + +var ErrLocalProjectNameConflict = errors.New("project name conflicts") + +func (r *ProjectRepo) Create(instance interface{}) error { + newProject := instance.(*entity.Project) + // TODO: check name conflicts? + // Add records + return db.Model(&entity.Project{}).Create(newProject).Error +} + +func (r *ProjectRepo) GetAll() (interface{}, error) { + var projectList []entity.Project + if err := db.Find(&projectList).Error; err != nil { + return nil, err + } + return projectList, nil +} + +func (r *ProjectRepo) GetByUUID(uuid string) (interface{}, error) { + project := &entity.Project{} + if err := db.Where("uuid = ?", uuid).First(&project).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, repo.ErrProjectNotFound + } + return nil, err + } + return project, nil +} + +func (r *ProjectRepo) DeleteByUUID(uuid string) error { + return db.Unscoped().Where("uuid = ?", uuid).Delete(&entity.Project{}).Error +} + +func (r *ProjectRepo) CheckNameConflict(name string) error { + var count int64 + err := db.Model(&entity.Project{}). + Where("name = ? AND status = ?", name, entity.ProjectStatusManaged). + Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return ErrLocalProjectNameConflict + } + return nil +} + +func (r *ProjectRepo) UpdateStatusByUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("uuid = ?", project.UUID). + Update("status", project.Status).Error +} + +func (r *ProjectRepo) UpdateTypeByUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("uuid = ?", project.UUID). + Update("type", project.Type).Error +} + +func (r *ProjectRepo) UpdateAutoApprovalStatusByUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("uuid = ?", project.UUID). + Update("auto_approval_enabled", project.AutoApprovalEnabled).Error +} + +func (r *ProjectRepo) UpdateManagingSiteInfoBySiteUUID(instance interface{}) error { + project := instance.(*entity.Project) + return db.Model(&entity.Project{}).Where("managing_site_uuid = ?", project.ManagingSiteUUID). + Select("managing_site_name", "managing_site_party_id").Updates(project).Error +} + +// InitTable make sure the table is created in the db +func (r *ProjectRepo) InitTable() { + if err := db.AutoMigrate(&entity.Project{}); err != nil { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/site_repo.go b/site-portal/server/infrastructure/gorm/site_repo.go new file mode 100644 index 00000000..6a6ff6c2 --- /dev/null +++ b/site-portal/server/infrastructure/gorm/site_repo.go @@ -0,0 +1,89 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" +) + +// SiteRepo is the implementation of the domain's repo interface +type SiteRepo struct{} + +var _ repo.SiteRepository = (*SiteRepo)(nil) + +// ErrSiteExist means the site info is already there +var ErrSiteExist = errors.New("site already configured") + +// Load reads the site information +func (r *SiteRepo) Load(instance interface{}) error { + site := instance.(*entity.Site) + return db.First(site).Error +} + +// GetSite returns the site information +func (r *SiteRepo) GetSite() (interface{}, error) { + var s entity.Site + if err := db.First(&s).Error; err != nil { + return nil, err + } + return &s, nil +} + +// CreateSite inserts a site info entry +func (r *SiteRepo) CreateSite(site *entity.Site) error { + var count int64 + db.Model(&entity.Site{}).Count(&count) + if count > 0 { + return ErrSiteExist + } + if err := db.Model(&entity.Site{}).Create(site).Error; err != nil { + return err + } + return nil +} + +// Update updates the site info +func (r *SiteRepo) Update(instance interface{}) error { + updatedSite := instance.(*entity.Site) + return db.Save(updatedSite).Error +} + +// UpdateFMLManagerConnectionStatus updates fml manager related information +func (r *SiteRepo) UpdateFMLManagerConnectionStatus(instance interface{}) error { + updatedSite := instance.(*entity.Site) + return db.Model(updatedSite).Where("id = ?", updatedSite.ID). + Select("fml_manager_endpoint", "fml_manager_server_name", "fml_manager_connected_at", "fml_manager_connected"). + Updates(updatedSite).Error +} + +// InitTable make sure the table is created in the db +func (r *SiteRepo) InitTable() { + if err := db.AutoMigrate(entity.Site{}); err != nil { + panic(err) + } +} + +// InitData inserts an empty site info +func (r *SiteRepo) InitData() { + site := &entity.Site{ + UUID: uuid.NewV4().String(), + } + if err := r.CreateSite(site); err != nil && err != ErrSiteExist { + panic(err) + } +} diff --git a/site-portal/server/infrastructure/gorm/user_repo.go b/site-portal/server/infrastructure/gorm/user_repo.go new file mode 100644 index 00000000..62c3065d --- /dev/null +++ b/site-portal/server/infrastructure/gorm/user_repo.go @@ -0,0 +1,180 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gorm + +import ( + "github.com/FederatedAI/FedLCM/site-portal/server/domain/entity" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/repo" + "github.com/FederatedAI/FedLCM/site-portal/server/domain/valueobject" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + uuid "github.com/satori/go.uuid" + "github.com/spf13/viper" + "golang.org/x/crypto/bcrypt" +) + +// UserRepo implements repo.UserRepository using gorm and PostgreSQL +type UserRepo struct{} + +// ErrUserExist means new user cannot be created due to the existence of the same-name user +var ErrUserExist = errors.New("user already exists") + +// make sure UserRepo implements the repo.UserRepository interface +var _ repo.UserRepository = (*UserRepo)(nil) + +// GetAllUsers returns all available users' info +func (r *UserRepo) GetAllUsers() (interface{}, error) { + var users []entity.User + if err := db.Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +// CreateUser creates a new user +func (r *UserRepo) CreateUser(instance interface{}) error { + // check name namespace + var count int64 + newUser := instance.(*entity.User) + db.Model(&entity.User{}).Where("name = ?", newUser.Name).Count(&count) + if count > 0 { + return ErrUserExist + } + + //Add data + if err := db.Model(&entity.User{}).Create(newUser).Error; err != nil { + return err + } + + return nil +} + +// UpdatePasswordById changes the user's hashed password +func (r *UserRepo) UpdatePasswordById(id uint, hashedPassword string) error { + toUpdateUser := &entity.User{} + if err := db.Where("id = ?", id).First(&toUpdateUser).Error; err != nil { + return err + } + return db.Model(toUpdateUser).Update("password", hashedPassword).Error +} + +// UpdatePermissionInfoById changes the specified user's permission info +func (r *UserRepo) UpdatePermissionInfoById(id uint, info valueobject.UserPermissionInfo) error { + toUpdateUser := &entity.User{} + if err := db.Where("id = ?", id).First(&toUpdateUser).Error; err != nil { + return err + } + toUpdateUser.PermissionInfo = info + return db.Model(&entity.User{}).Where("id = ?", id). + Select("site_portal_access", "fateboard_access", "notebook_access").Updates(toUpdateUser).Error +} + +// UpdateByName changes the specified user's info +func (r *UserRepo) UpdateByName(updatedUser *entity.User) error { + return db.Model(&entity.User{}).Where("name = ?", updatedUser.Name).Updates(updatedUser).Error +} + +// GetByName returns the user info indexed by the name +func (r *UserRepo) GetByName(name string) (*entity.User, error) { + user := &entity.User{} + if err := db.Where("name = ?", name).First(&user).Error; err != nil { + return nil, err + } + return user, nil +} + +// LoadById loads the user info by id +func (r *UserRepo) LoadById(instance interface{}) error { + user := instance.(*entity.User) + return db.Where("id = ?", user.ID).First(&user).Error +} + +// LoadByName loads the user info by name +func (r *UserRepo) LoadByName(instance interface{}) error { + user := instance.(*entity.User) + return db.Where("name = ?", user.Name).First(&user).Error +} + +// InitTable make sure the table is created in the db +func (r *UserRepo) InitTable() { + if err := db.AutoMigrate(entity.User{}); err != nil { + panic(err) + } +} + +// InitData inserts or updates the defaults users information +func (r *UserRepo) InitData() { + + adminPassword := viper.GetString("siteportal.initial.admin.password") + if adminPassword == "" { + adminPassword = "admin" + } + userPassword := viper.GetString("siteportal.initial.user.password") + if userPassword == "" { + userPassword = "user" + } + + hashedAdminPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + hashedUserPassword, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) + if err != nil { + panic(err) + } + + admin := &entity.User{ + UUID: uuid.NewV4().String(), + Name: "Admin", + Password: string(hashedAdminPassword), + PermissionInfo: valueobject.UserPermissionInfo{ + SitePortalAccess: true, + FATEBoardAccess: true, + NotebookAccess: true, + }, + } + + user := &entity.User{ + UUID: uuid.NewV4().String(), + Name: "User", + Password: string(hashedUserPassword), + PermissionInfo: valueobject.UserPermissionInfo{ + SitePortalAccess: true, + FATEBoardAccess: true, + NotebookAccess: true, + }, + } + + createOrUpdatePassword := func(user *entity.User) error { + if err := r.CreateUser(user); err != nil { + if err == ErrUserExist { + log.Info().Msgf("user: %s exists", user.Name) + } else { + return err + } + } else { + log.Info().Msgf("user: %s created with default credentials", user.Name) + } + return nil + } + + if err := createOrUpdatePassword(admin); err != nil { + panic(err) + } + if err := createOrUpdatePassword(user); err != nil { + panic(err) + } + +} diff --git a/site-portal/server/infrastructure/kubernetes/client.go b/site-portal/server/infrastructure/kubernetes/client.go new file mode 100644 index 00000000..7251f792 --- /dev/null +++ b/site-portal/server/infrastructure/kubernetes/client.go @@ -0,0 +1,61 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type kubernetesClient struct { + dynamicClient dynamic.Interface + clientSet *kubernetes.Clientset +} + +// NewKubernetesClient creates a new client instance to work with a kubernetes cluster +func NewKubernetesClient(kubeconfigPath string) (*kubernetesClient, error) { + config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return nil, err + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + return &kubernetesClient{ + dynamicClient: dynamicClient, + clientSet: clientSet, + }, nil +} + +// GetInferenceServiceList uses dynamic client to retrieve the isvc +func (c *kubernetesClient) GetInferenceServiceList() (*unstructured.UnstructuredList, error) { + isvcRes := schema.GroupVersionResource{ + Group: "serving.kubeflow.org", + Version: "v1beta1", + Resource: "inferenceservices", + } + return c.dynamicClient.Resource(isvcRes).List(context.TODO(), v1.ListOptions{}) +} diff --git a/site-portal/server/main.go b/site-portal/server/main.go new file mode 100644 index 00000000..4ab8edf4 --- /dev/null +++ b/site-portal/server/main.go @@ -0,0 +1,241 @@ +// Copyright 2022 VMware, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/FederatedAI/FedLCM/site-portal/server/api" + "github.com/FederatedAI/FedLCM/site-portal/server/constants" + "github.com/FederatedAI/FedLCM/site-portal/server/infrastructure/gorm" + "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/utils/logging" + "github.com/gin-contrib/logger" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + // swag API + _ "github.com/FederatedAI/FedLCM/site-portal/server/docs" +) + +// FRONTEND_DIR is the folder where the frontend static file resides +var FRONTEND_DIR = getFrontendDir() + +// main starts the API server +// @title site portal API service +// @version v1 +// @description backend APIs of site portal service +// @termsOfService http://swagger.io/terms/ +// @contact.name FedLCM team +// @BasePath /api/v1 +// @in header +func main() { + viper.AutomaticEnv() + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) + + log.Logger = log.Output( + zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }, + ).With().Caller().Stack().Logger().Level(zerolog.InfoLevel) + debugLog, _ := strconv.ParseBool(viper.GetString("siteportal.debug")) + if debugLog { + log.Logger = log.Logger.Level(zerolog.DebugLevel) + } + + if err := initDB(); err != nil { + panic(err) + } + r := createGinEngine() + initRouter(r) + + tlsEnabled := viper.GetBool("siteportal.tls.enabled") + if tlsEnabled { + sitePortalServerCert := viper.GetString("siteportal.tls.server.cert") + sitePortalServerKey := viper.GetString("siteportal.tls.server.key") + caCertPath := viper.GetString("siteportal.tls.ca.cert") + pool := x509.NewCertPool() + caCrt, err := ioutil.ReadFile(caCertPath) + if err != nil { + log.Error().Err(err).Msg("read ca.crt file error") + } + pool.AppendCertsFromPEM(caCrt) + tlsConfig := &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: pool, + } + tlsPort := viper.GetString("siteportal.tls.port") + if tlsPort == "" { + tlsPort = "8443" + } + server := http.Server{ + Addr: ":" + tlsPort, + Handler: r, + TLSConfig: tlsConfig, + } + log.Info().Msgf("Listening and serving HTTPs on %s", server.Addr) + if err := server.ListenAndServeTLS(sitePortalServerCert, sitePortalServerKey); err != nil { + log.Error().Err(err).Msg("server run error with TLS, ") + return + } + } else { + err := r.Run() + if err != nil { + log.Error().Err(err).Msg("gin run error, ") + return + } + } +} + +func createGinEngine() *gin.Engine { + r := gin.New() + + r.Use(gin.Recovery()) + r.Use(logger.SetLogger( + logger.WithUTC(true), + logger.WithLogger(logging.GetGinLogger))) + + return r +} + +func initDB() error { + var err error + + for i := 0; i < 3; i++ { + err = gorm.InitDB() + if err == nil { + return nil + } + time.Sleep(5 * time.Second) + } + + return fmt.Errorf("initialization failed: %s", err) +} + +func initRouter(r *gin.Engine) { + + r.NoRoute(func(c *gin.Context) { + dir, file := path.Split(c.Request.RequestURI) + ext := filepath.Ext(file) + if file == "" || ext == "" { + c.File(filepath.Join(FRONTEND_DIR, "index.html")) + } else { + c.File(filepath.Join(FRONTEND_DIR, dir, file)) + } + }) + + v1 := r.Group("/api/" + constants.APIVersion) + { + v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + v1.GET("/status", func(c *gin.Context) { + c.JSON(200, gin.H{ + "msg": "The service is running", + "api_version": constants.APIVersion, + "source_commit": constants.Commit, + "source_branch": constants.Branch, + "build_time": constants.BuildTime, + }) + }) + + // user management + userRepo := &gorm.UserRepo{} + userRepo.InitTable() + userRepo.InitData() + // create authMiddleware before any other controllers + if err := api.CreateAuthMiddleware(userRepo); err != nil { + panic(err) + } + api.NewUserController(userRepo).Route(v1) + + // site management + siteRepo := &gorm.SiteRepo{} + siteRepo.InitTable() + siteRepo.InitData() + api.NewSiteController(siteRepo).Route(v1) + + // local data management repo + localDataRepo := &gorm.LocalDataRepo{} + localDataRepo.InitTable() + + // project management repo + projectRepo := &gorm.ProjectRepo{} + projectRepo.InitTable() + projectParticipantRepo := &gorm.ProjectParticipantRepo{} + projectParticipantRepo.InitTable() + projectInvitationRepo := &gorm.ProjectInvitationRepo{} + projectInvitationRepo.InitTable() + projectDataRepo := &gorm.ProjectDataRepo{} + projectDataRepo.InitTable() + + // job management repo + jobRepo := &gorm.JobRepo{} + jobRepo.InitTable() + jobParticipantRepo := &gorm.JobParticipantRepo{} + jobParticipantRepo.InitTable() + + // model management repo + modelRepo := &gorm.ModelRepo{} + modelRepo.InitTable() + + // model deployment repo + modelDeploymentRepo := &gorm.ModelDeploymentRepo{} + modelDeploymentRepo.InitTable() + + // local data management + api.NewLocalDataController(localDataRepo, siteRepo, projectRepo, projectDataRepo).Route(v1) + + // project management + api.NewProjectController(projectRepo, siteRepo, projectParticipantRepo, + projectInvitationRepo, projectDataRepo, localDataRepo, jobRepo, jobParticipantRepo, + modelRepo).Route(v1) + + // job management + api.NewJobController(jobRepo, jobParticipantRepo, projectRepo, siteRepo, projectDataRepo, modelRepo).Route(v1) + + // model management + api.NewModelController(modelRepo, modelDeploymentRepo, siteRepo, projectRepo).Route(v1) + } +} + +func getFrontendDir() (frontendDir string) { + frontendDir = os.Getenv("FRONTEND_DIR") + if frontendDir != "" { + return + } + exe, err := os.Executable() + if err != nil { + panic(err) + } + exePath := filepath.Dir(exe) + frontendDir = filepath.Join(exePath, "frontend") + return +} diff --git a/site-portal/tls/nginx.conf.template b/site-portal/tls/nginx.conf.template new file mode 100644 index 00000000..0b9a1c4a --- /dev/null +++ b/site-portal/tls/nginx.conf.template @@ -0,0 +1,70 @@ + +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + + client_body_temp_path /tmp/client_body_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + upstream server { + server ${SITEPORTAL_SERVER_HOST}:8443; + } + + server { + listen 8443 ssl; + server_name localhost; + + ssl_certificate /var/lib/site-portal/cert/server.crt; + ssl_certificate_key /var/lib/site-portal/cert/server.key; + ssl_client_certificate /var/lib/site-portal/cert/ca.crt; + ssl_verify_client optional; + + ssl_protocols TLSv1.2; + ssl_ciphers '!aNULL:kECDH+AESGCM:ECDH+AESGCM:RSA+AESGCM:kECDH+AES:ECDH+AES:RSA+AES:'; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /api/ { + proxy_pass https://server/api/; + + proxy_ssl_certificate /var/lib/site-portal/cert/client.crt; + proxy_ssl_certificate_key /var/lib/site-portal/cert/client.key; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_set_header X-SP-CLIENT-CERT $ssl_client_escaped_cert; + proxy_set_header X-SP-CLIENT-SDN $ssl_client_s_dn; + proxy_set_header X-SP-CLIENT-VERIFY $ssl_client_verify; + } + } +} \ No newline at end of file