From cd07e1cb81f58dc440819c1290edff07db9513a2 Mon Sep 17 00:00:00 2001 From: Chenlong Ma Date: Mon, 30 Jan 2023 15:03:48 +0800 Subject: [PATCH 01/16] Create workflows (#4) Signed-off-by: Chenlong Ma --- .github/workflows/CodeQL.yml | 72 +++++++++++++++++++ .github/workflows/check-crlf.yaml | 14 ++++ .../fedlcm-docker-build-and-push.yaml | 50 +++++++++++++ .github/workflows/fedlcm-unit-test.yaml | 25 +++++++ .../fml-manager-docker-build-and push.yaml | 52 ++++++++++++++ .github/workflows/fml-manager-unit-test.yaml | 26 +++++++ .../site-portal-docker-build-and-push.yaml | 52 ++++++++++++++ .github/workflows/site-portal-unit-test.yaml | 26 +++++++ 8 files changed, 317 insertions(+) create mode 100644 .github/workflows/CodeQL.yml create mode 100644 .github/workflows/check-crlf.yaml create mode 100644 .github/workflows/fedlcm-docker-build-and-push.yaml create mode 100644 .github/workflows/fedlcm-unit-test.yaml create mode 100644 .github/workflows/fml-manager-docker-build-and push.yaml create mode 100644 .github/workflows/fml-manager-unit-test.yaml create mode 100644 .github/workflows/site-portal-docker-build-and-push.yaml create mode 100644 .github/workflows/site-portal-unit-test.yaml diff --git a/.github/workflows/CodeQL.yml b/.github/workflows/CodeQL.yml new file mode 100644 index 0000000..e8fb4c0 --- /dev/null +++ b/.github/workflows/CodeQL.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/check-crlf.yaml b/.github/workflows/check-crlf.yaml new file mode 100644 index 0000000..8f2abbc --- /dev/null +++ b/.github/workflows/check-crlf.yaml @@ -0,0 +1,14 @@ +name: Check CRLF + +on: pull_request + +jobs: + check-CRLF: + name: Check CRLF action + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@main + + - name: check-crlf + uses: erclu/check-crlf@v1 \ No newline at end of file diff --git a/.github/workflows/fedlcm-docker-build-and-push.yaml b/.github/workflows/fedlcm-docker-build-and-push.yaml new file mode 100644 index 0000000..957f4a7 --- /dev/null +++ b/.github/workflows/fedlcm-docker-build-and-push.yaml @@ -0,0 +1,50 @@ +name: FedLCM docker build and push + +on: + push: + # Publish `main` as Docker `latest` image. + branches: + - main + + # Publish `v1.2.3` tags as releases. + tags: + - v* + +jobs: + # no test is required + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@main + + - name: Prepare the TAG + id: prepare-the-tag + run: | + # strip git ref prefix from version + TAG="" + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + if [ $VERSION = "main" ]; then + TAG="latest" + fi + echo "TAG=${TAG}" + echo "TAG=${TAG}" >> $GITHUB_OUTPUT + - name: Build image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + make docker-build + + - name: Log into DockerHub + run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + make docker-push diff --git a/.github/workflows/fedlcm-unit-test.yaml b/.github/workflows/fedlcm-unit-test.yaml new file mode 100644 index 0000000..f345c84 --- /dev/null +++ b/.github/workflows/fedlcm-unit-test.yaml @@ -0,0 +1,25 @@ +name: FedLCM server unit test + +on: + pull_request: + paths: + - ".github/workflows/fedlcm-unit-test.yaml" + - "server/*" + - “pkg/*” +jobs: + Unit-test: + name: Unit Test + runs-on: ubuntu-latest + steps: + - name: Setup + uses: actions/setup-go@v1 + with: + go-version: 1.19 + id: go + + - name: Code + uses: actions/checkout@main + + - name: Unit Test + run: | + make server-unittest \ No newline at end of file diff --git a/.github/workflows/fml-manager-docker-build-and push.yaml b/.github/workflows/fml-manager-docker-build-and push.yaml new file mode 100644 index 0000000..4a58ee6 --- /dev/null +++ b/.github/workflows/fml-manager-docker-build-and push.yaml @@ -0,0 +1,52 @@ +name: FML-Manager docker build and push + +on: + push: + # Publish `main` as Docker `latest` image. + branches: + - main + + # Publish `v1.2.3` tags as releases. + tags: + - v* + +jobs: + # no test is required + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@main + + - name: Prepare the TAG + id: prepare-the-tag + run: | + # strip git ref prefix from version + TAG="" + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + if [ $VERSION = "main" ]; then + TAG="latest" + fi + echo "TAG=${TAG}" + echo "TAG=${TAG}" >> $GITHUB_OUTPUT + - name: Build image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + cd fml-manager + make docker-build + + - name: Log into DockerHub + run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + cd fml-manager + make docker-push diff --git a/.github/workflows/fml-manager-unit-test.yaml b/.github/workflows/fml-manager-unit-test.yaml new file mode 100644 index 0000000..fcbc456 --- /dev/null +++ b/.github/workflows/fml-manager-unit-test.yaml @@ -0,0 +1,26 @@ +name: FML-Manager server unit test + +on: + pull_request: + paths: + - ".github/workflows/fml-manager-unit-test.yaml" + - "fml-manager/server/*" + +jobs: + Unit-test: + name: Unit Test + runs-on: ubuntu-latest + steps: + - name: Setup + uses: actions/setup-go@v1 + with: + go-version: 1.19 + id: go + + - name: Code + uses: actions/checkout@main + + - name: Unit Test + run: | + cd fml-manager + make server-unittest \ No newline at end of file diff --git a/.github/workflows/site-portal-docker-build-and-push.yaml b/.github/workflows/site-portal-docker-build-and-push.yaml new file mode 100644 index 0000000..443566d --- /dev/null +++ b/.github/workflows/site-portal-docker-build-and-push.yaml @@ -0,0 +1,52 @@ +name: Site-Portal docker build and push + +on: + push: + # Publish `main` as Docker `latest` image. + branches: + - main + + # Publish `v1.2.3` tags as releases. + tags: + - v* + +jobs: + # no test is required + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@main + + - name: Prepare the TAG + id: prepare-the-tag + run: | + # strip git ref prefix from version + TAG="" + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + if [ $VERSION = "main" ]; then + TAG="latest" + fi + echo "TAG=${TAG}" + echo "TAG=${TAG}" >> $GITHUB_OUTPUT + - name: Build image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + cd site-portal + make docker-build + + - name: Log into DockerHub + run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push image + run: | + TAG=${{steps.prepare-the-tag.outputs.TAG}} + if [ ! -z "$TAG" ]; then + export TAG=$TAG + fi + cd site-portal + make docker-push diff --git a/.github/workflows/site-portal-unit-test.yaml b/.github/workflows/site-portal-unit-test.yaml new file mode 100644 index 0000000..2eec508 --- /dev/null +++ b/.github/workflows/site-portal-unit-test.yaml @@ -0,0 +1,26 @@ +name: Site-Portal server unit test + +on: + pull_request: + paths: + - ".github/workflows/site-portal-unit-test.yaml" + - "site-portal/server/*" + +jobs: + Unit-test: + name: Unit Test + runs-on: ubuntu-latest + steps: + - name: Setup + uses: actions/setup-go@v1 + with: + go-version: 1.19 + id: go + + - name: Code + uses: actions/checkout@main + + - name: Unit Test + run: | + cd site-portal + make server-unittest \ No newline at end of file From 9f1b556586f2cbab3c73d0fe80bc563d34ce78a7 Mon Sep 17 00:00:00 2001 From: Chenlong Ma Date: Mon, 13 Feb 2023 17:09:26 +0800 Subject: [PATCH 02/16] Support FATE 1.10 (#7) * FedLCM and Site-Portal support FATE 1.10.0 Signed-off-by: Chenlong Ma * Sort the chart list for easy viewing. Signed-off-by: Chenlong Ma * Fix matching rules for workflows path Signed-off-by: Chenlong Ma * Fix go unit test Signed-off-by: Chenlong Ma * update version tag Signed-off-by: Chenlong Ma --------- Signed-off-by: Chenlong Ma --- .env | 2 +- .github/workflows/fedlcm-unit-test.yaml | 4 +- .github/workflows/fml-manager-unit-test.yaml | 2 +- .github/workflows/site-portal-unit-test.yaml | 2 +- Makefile | 2 +- fml-manager/.env | 2 +- fml-manager/Makefile | 2 +- helm-charts/charts/fate-exchange/Chart.yaml | 4 +- .../templates/fml-manager/_helpers.tpl | 2 +- .../values-template-example.yaml | 5 +- helm-charts/charts/fate-exchange/values.yaml | 9 +- helm-charts/charts/fate-package-and-base64.sh | 7 + helm-charts/charts/fate/Chart.yaml | 5 +- .../templates/core/client/statefulSet.yaml | 5 +- .../charts/fate/templates/core/fateboard.yaml | 106 + .../templates/core/fateboard/configmap.yaml | 5 + .../templates/core/fateboard/service.yaml | 4 +- .../templates/core/fateflow/configmap.yaml | 53 +- .../fate/templates/core/fateflow/service.yaml | 2 - .../fate/templates/core/python-spark.yaml | 56 +- .../fate/templates/site-portal/_helpers.tpl | 2 +- .../charts/fate/values-template-example.yaml | 182 +- helm-charts/charts/fate/values-template.yaml | 33 +- helm-charts/charts/fate/values.yaml | 45 +- helm-charts/fml-manager.yaml | 2 +- helm-charts/site-portal.yaml | 6 +- k8s_deploy.yaml | 4 +- server/api/federation.go | 1 + server/domain/entity/chart.go | 14 + .../service/participant_fate_service_test.go | 363 +-- server/infrastructure/gorm/chart_mock_repo.go | 2588 +++++++++++++++-- .../gorm/mock/chart_fate_1_10_0.go | 33 + site-portal/.env | 2 +- site-portal/Makefile | 2 +- 34 files changed, 2754 insertions(+), 802 deletions(-) create mode 100644 helm-charts/charts/fate-package-and-base64.sh create mode 100644 helm-charts/charts/fate/templates/core/fateboard.yaml create mode 100644 server/infrastructure/gorm/mock/chart_fate_1_10_0.go diff --git a/.env b/.env index 43770b2..f1ad1d9 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -TAG=v0.2.0 +TAG=v0.3.0 SERVER_NAME=federatedai/fedlcm-server SERVER_IMG=${SERVER_NAME}:${TAG} diff --git a/.github/workflows/fedlcm-unit-test.yaml b/.github/workflows/fedlcm-unit-test.yaml index f345c84..e23fe17 100644 --- a/.github/workflows/fedlcm-unit-test.yaml +++ b/.github/workflows/fedlcm-unit-test.yaml @@ -4,8 +4,8 @@ on: pull_request: paths: - ".github/workflows/fedlcm-unit-test.yaml" - - "server/*" - - “pkg/*” + - "server/**" + - “pkg/**” jobs: Unit-test: name: Unit Test diff --git a/.github/workflows/fml-manager-unit-test.yaml b/.github/workflows/fml-manager-unit-test.yaml index fcbc456..3fc5c1b 100644 --- a/.github/workflows/fml-manager-unit-test.yaml +++ b/.github/workflows/fml-manager-unit-test.yaml @@ -4,7 +4,7 @@ on: pull_request: paths: - ".github/workflows/fml-manager-unit-test.yaml" - - "fml-manager/server/*" + - "fml-manager/server/**" jobs: Unit-test: diff --git a/.github/workflows/site-portal-unit-test.yaml b/.github/workflows/site-portal-unit-test.yaml index 2eec508..66b87be 100644 --- a/.github/workflows/site-portal-unit-test.yaml +++ b/.github/workflows/site-portal-unit-test.yaml @@ -4,7 +4,7 @@ on: pull_request: paths: - ".github/workflows/site-portal-unit-test.yaml" - - "site-portal/server/*" + - "site-portal/server/**" jobs: Unit-test: diff --git a/Makefile b/Makefile index 9331326..a89a798 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .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.2.0 +TAG ?= v0.3.0 SERVER_NAME ?= federatedai/fedlcm-server SERVER_IMG ?= ${SERVER_NAME}:${TAG} diff --git a/fml-manager/.env b/fml-manager/.env index b6aa0c6..843db87 100644 --- a/fml-manager/.env +++ b/fml-manager/.env @@ -1,4 +1,4 @@ -TAG=v0.2.0 +TAG=v0.3.0 SERVER_NAME=federatedai/fml-manager-server SERVER_IMG=${SERVER_NAME}:${TAG} \ No newline at end of file diff --git a/fml-manager/Makefile b/fml-manager/Makefile index bc098bf..74a1a67 100644 --- a/fml-manager/Makefile +++ b/fml-manager/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean format swag swag-bin server-unittest server run RELEASE_VERSION ?= ${shell git describe --tags} -TAG ?= v0.2.0 +TAG ?= v0.3.0 SERVER_NAME ?= federatedai/fml-manager-server SERVER_IMG ?= ${SERVER_NAME}:${TAG} diff --git a/helm-charts/charts/fate-exchange/Chart.yaml b/helm-charts/charts/fate-exchange/Chart.yaml index 2425ade..fcab400 100644 --- a/helm-charts/charts/fate-exchange/Chart.yaml +++ b/helm-charts/charts/fate-exchange/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: "exchangev1.9.1 & fedlcmv0.2.0" +appVersion: "exchangev1.10.0 & fedlcmv0.3.0" description: A Helm chart for fate exchange and fml-manager name: fate-exchange -version: v1.9.1-fedlcm-v0.2.0 +version: v1.10.0-fedlcm-v0.3.0 diff --git a/helm-charts/charts/fate-exchange/templates/fml-manager/_helpers.tpl b/helm-charts/charts/fate-exchange/templates/fml-manager/_helpers.tpl index 07a5ec3..e5acd3e 100644 --- a/helm-charts/charts/fate-exchange/templates/fml-manager/_helpers.tpl +++ b/helm-charts/charts/fate-exchange/templates/fml-manager/_helpers.tpl @@ -12,5 +12,5 @@ {{/* Images Tag: According to the actual version of siteportal */}} {{- define "fmlManager.images.tag" -}} -v0.2.0 +v0.3.0 {{- end -}} diff --git a/helm-charts/charts/fate-exchange/values-template-example.yaml b/helm-charts/charts/fate-exchange/values-template-example.yaml index 30d0273..c2a02d5 100644 --- a/helm-charts/charts/fate-exchange/values-template-example.yaml +++ b/helm-charts/charts/fate-exchange/values-template-example.yaml @@ -1,9 +1,10 @@ name: fate-exchange namespace: fate-exchange chartName: fate-exchange -chartVersion: v1.9.1-fedlcm-v0.2.0 -partyId: 0 +chartVersion: v1.10.0-fedlcm-v0.3.0 +partyId: 1 registry: "" +pullPolicy: imagePullSecrets: - name: myregistrykey persistence: false diff --git a/helm-charts/charts/fate-exchange/values.yaml b/helm-charts/charts/fate-exchange/values.yaml index 2b05f1a..5ad5425 100644 --- a/helm-charts/charts/fate-exchange/values.yaml +++ b/helm-charts/charts/fate-exchange/values.yaml @@ -4,13 +4,10 @@ partyName: fate-exchange image: registry: federatedai isThridParty: - tag: 1.9.1-release + tag: 1.10.0-release pullPolicy: IfNotPresent - imagePullSecrets: + imagePullSecrets: # - name: - -partyId: 9999 -partyName: fate-9999 podSecurityPolicy: enabled: false @@ -110,7 +107,7 @@ modules: fmlManagerServer: include: true image: federatedai/fml-manager-server - imageTag: v0.2.0 + imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: diff --git a/helm-charts/charts/fate-package-and-base64.sh b/helm-charts/charts/fate-package-and-base64.sh new file mode 100644 index 0000000..5b678e8 --- /dev/null +++ b/helm-charts/charts/fate-package-and-base64.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +helm package fate-exchange +helm package fate + +base64 -i fate-exchange-v*.tgz > fate-exchange-base64.txt +base64 -i fate-v*.tgz > fate-base64.txt diff --git a/helm-charts/charts/fate/Chart.yaml b/helm-charts/charts/fate/Chart.yaml index 982c5fb..f0c571d 100644 --- a/helm-charts/charts/fate/Chart.yaml +++ b/helm-charts/charts/fate/Chart.yaml @@ -1,8 +1,9 @@ apiVersion: v1 -appVersion: "fatev1.9.1+fedlcmv0.2.0" +appVersion: "fatev1.10.0+fedlcmv0.3.0" description: Helm chart for FATE and site-portal in FedLCM name: fate -version: v1.9.1-fedlcm-v0.2.0 +version: v1.10.0-fedlcm-v0.3.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 diff --git a/helm-charts/charts/fate/templates/core/client/statefulSet.yaml b/helm-charts/charts/fate/templates/core/client/statefulSet.yaml index bb0ef7d..83f218d 100644 --- a/helm-charts/charts/fate/templates/core/client/statefulSet.yaml +++ b/helm-charts/charts/fate/templates/core/client/statefulSet.yaml @@ -43,8 +43,11 @@ spec: value: "9380" - name: FATE_SERVING_HOST value: "{{.Values.modules.serving.ip}}:{{.Values.modules.serving.port}}" + - name: NOTEBOOK_HASHED_PASSWORD + value: {{ .Values.modules.client.notebook_hashed_password }} ports: - containerPort: 20000 + command: ["bash", "-c", "pipeline init --ip ${FATE_FLOW_IP} --port ${FATE_FLOW_PORT} && flow init --ip ${FATE_FLOW_IP} --port ${FATE_FLOW_PORT} && jupyter notebook --ip=0.0.0.0 --port=20000 --allow-root --debug --NotebookApp.notebook_dir='/data/projects/fate/' --no-browser --NotebookApp.token='' --NotebookApp.password=${NOTEBOOK_HASHED_PASSWORD}"] livenessProbe: httpGet: path: / @@ -122,4 +125,4 @@ spec: requests: storage: {{ .Values.modules.client.size }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm-charts/charts/fate/templates/core/fateboard.yaml b/helm-charts/charts/fate/templates/core/fateboard.yaml new file mode 100644 index 0000000..7c16c9e --- /dev/null +++ b/helm-charts/charts/fate/templates/core/fateboard.yaml @@ -0,0 +1,106 @@ +# Copyright 2019-2022 VMware, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.fateboard.include }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fateboard + labels: + fateMoudle: fateboard +{{ include "fate.labels" . | indent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + fateMoudle: fateboard +{{ include "fate.matchLabels" . | indent 6 }} + template: + metadata: + annotations: + {{- if .Values.istio.enabled }} + sidecar.istio.io/rewriteAppHTTPProbers: "false" + {{- end }} + labels: + fateMoudle: fateboard +{{ include "fate.labels" . | indent 8 }} + spec: + containers: + {{- if .Values.modules.fateboard.include }} + - image: {{ .Values.image.registry }}/fateboard:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + name: fateboard + ports: + - containerPort: 8080 + livenessProbe: + httpGet: + path: / + port: 8080 + httpHeaders: + - name: X-Custom-Header + value: livenessProbe + initialDelaySeconds: 1 + periodSeconds: 10 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 8080 + httpHeaders: + - name: X-Custom-Header + value: readinessProbe + initialDelaySeconds: 1 + periodSeconds: 10 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + startupProbe: + httpGet: + path: / + port: 8080 + httpHeaders: + - name: X-Custom-Header + value: startupProbe + failureThreshold: 12 + periodSeconds: 10 + volumeMounts: + - mountPath: /data/projects/fate/fateboard/conf/application.properties + name: fateboard-confs + subPath: application.properties + {{- end }} + {{- with .Values.modules.fateboard.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.fateboard.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.modules.fateboard.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.image.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 6 }} + {{- end }} + restartPolicy: Always + volumes: + {{- if .Values.modules.fateboard.include }} + - name: fateboard-confs + configMap: + name: fateboard-config + {{- end }} +{{- end }} diff --git a/helm-charts/charts/fate/templates/core/fateboard/configmap.yaml b/helm-charts/charts/fate/templates/core/fateboard/configmap.yaml index a7bd717..51098d1 100644 --- a/helm-charts/charts/fate/templates/core/fateboard/configmap.yaml +++ b/helm-charts/charts/fate/templates/core/fateboard/configmap.yaml @@ -24,6 +24,11 @@ data: #priority is higher than {fateflow.url}, split by ; #below config can support configuring more than one fate flow for this fate board fateflow.url-list= + {{- $replicaCount := .Values.modules.python.replicas | int -}} + {{- range $index0 := until $replicaCount }} + {{- $index1 := $index0 | add1 -}} + http://python-{{ $index0 }}.fateflow:9380{{ if ne $index1 $replicaCount }};{{ end }} + {{- end }} fateflow.http_app_key= fateflow.http_secret_key= server.servlet.encoding.charset=UTF-8 diff --git a/helm-charts/charts/fate/templates/core/fateboard/service.yaml b/helm-charts/charts/fate/templates/core/fateboard/service.yaml index 5059f5b..0920ff8 100644 --- a/helm-charts/charts/fate/templates/core/fateboard/service.yaml +++ b/helm-charts/charts/fate/templates/core/fateboard/service.yaml @@ -15,7 +15,7 @@ kind: Service metadata: name: fateboard labels: - fateMoudle: python + fateMoudle: fateboard {{ include "fate.labels" . | indent 4 }} spec: ports: @@ -25,6 +25,6 @@ spec: protocol: TCP type: {{ .Values.modules.fateboard.type }} selector: - fateMoudle: python + fateMoudle: fateboard {{ include "fate.matchLabels" . | indent 4 }} {{- end }} \ No newline at end of file diff --git a/helm-charts/charts/fate/templates/core/fateflow/configmap.yaml b/helm-charts/charts/fate/templates/core/fateflow/configmap.yaml index 298218d..3488c7a 100644 --- a/helm-charts/charts/fate/templates/core/fateflow/configmap.yaml +++ b/helm-charts/charts/fate/templates/core/fateflow/configmap.yaml @@ -44,7 +44,7 @@ data: service_conf.yaml: | use_registry: {{ .Values.modules.serving.useRegistry | default false }} use_deserialize_safe_module: false - dependent_distribution: false + dependent_distribution: {{ .Values.modules.python.dependent_distribution | default false }} encrypt_password: false encrypt_module: fate_arch.common.encrypt_utils#pwdecrypt private_key: @@ -67,9 +67,19 @@ data: dataset: false fateflow: # you must set real ip address, 127.0.0.1 and 0.0.0.0 is not supported - host: fateflow + host: fateflow_ip http_port: 9380 grpc_port: 9360 + # when you have multiple fateflow server on one party, + # we suggest using nginx for load balancing. + nginx: + # under K8s mode, 'fateflow' is the service name, which will be a L4 load balancer. + host: fateflow + http_port: 9380 + grpc_port: 9360 + # use random instance_id instead of {host}:{http_port} + random_instance_id: false + # 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 @@ -99,13 +109,6 @@ data: port: {{ .Values.externalMysqlPort | default .Values.modules.mysql.port | default "3306" }} max_connections: 100 stale_timeout: 30 - zookeeper: - hosts: - - "serving-zookeeper:2181" - # use_acl: false - # user: fate - # password: fate - # engine services default_engines: {{- if eq .Values.computing "Spark_local" }} computing: "spark" @@ -139,11 +142,11 @@ data: token_code: MLSS python_path: /data/projects/fate/python hive: - host: 127.0.0.1 - port: 10000 - auth_mechanism: - username: - password: + host: {{ .Values.modules.python.hive.host }} + port: {{ .Values.modules.python.hive.port }} + auth_mechanism: {{ .Values.modules.python.hive.auth_mechanism }} + username: {{ .Values.modules.python.hive.username }} + password: {{ .Values.modules.python.hive.password }} linkis_hive: host: 127.0.0.1 port: 9001 @@ -166,7 +169,9 @@ data: host: {{ .Values.modules.python.pulsar.host }} port: {{ .Values.modules.python.pulsar.port }} mng_port: {{ .Values.modules.python.pulsar.mng_port }} - topic_ttl: 3 + topic_ttl: {{ .Values.modules.python.pulsar.topic_ttl }} + cluster: {{ .Values.modules.python.pulsar.cluster }} + tenant: {{ .Values.modules.python.pulsar.tenant }} # default conf/pulsar_route_table.yaml route_table: conf/pulsar_route_table/pulsar_route_table.yaml # mode: replication / client, default: replication @@ -179,14 +184,14 @@ data: fateboard: host: fateboard port: 8080 - enable_model_store: false + enable_model_store: true model_store_address: storage: mysql - name: {{ .Values.externalMysqlDatabase | default .Values.modules.mysql.database | default "eggroll_meta" }} + database: {{ .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" }}' + password: '{{ .Values.externalMysqlPassword | default .Values.modules.mysql.password | default "fate_dev" }}' max_connections: 10 stale_timeout: 10 {{- with .Values.modules.serving }} @@ -197,9 +202,15 @@ data: {{- else }} - '' {{- end }} - {{- if and .useRegistry .zookeeper }} zookeeper: + {{- if .zookeeper }} {{ toYaml .zookeeper | indent 6 }} + {{- else}} + hosts: + - serving-zookeeper.fate-serving-9999:2181 + use_acl: false + user: fate + password: fate {{- end }} {{- end }} transfer_conf.yaml: | @@ -246,8 +257,8 @@ data: federated_command_trys: 3 end_status_job_scheduling_time_limit: 300000 # ms end_status_job_scheduling_updates: 1 - auto_retries: 0 - auto_retry_delay: 1 #seconds + auto_retries: {{ .Values.modules.python.failedTaskAutoRetryTimes }} + auto_retry_delay: {{ .Values.modules.python.failedTaskAutoRetryDelay }} #seconds # It can also be specified in the job configuration using the federated_status_collect_type parameter federated_status_collect_type: PUSH detect_connect_max_retry_count: 3 diff --git a/helm-charts/charts/fate/templates/core/fateflow/service.yaml b/helm-charts/charts/fate/templates/core/fateflow/service.yaml index e2d7bce..a94757b 100644 --- a/helm-charts/charts/fate/templates/core/fateflow/service.yaml +++ b/helm-charts/charts/fate/templates/core/fateflow/service.yaml @@ -57,11 +57,9 @@ spec: {{- 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 }} diff --git a/helm-charts/charts/fate/templates/core/python-spark.yaml b/helm-charts/charts/fate/templates/core/python-spark.yaml index ca14a14..ce597d5 100644 --- a/helm-charts/charts/fate/templates/core/python-spark.yaml +++ b/helm-charts/charts/fate/templates/core/python-spark.yaml @@ -19,7 +19,7 @@ metadata: {{ include "fate.labels" . | indent 4 }} spec: serviceName: fateflow - replicas: 1 + replicas: {{ .Values.modules.python.replicas }} selector: matchLabels: fateMoudle: python @@ -123,7 +123,7 @@ spec: cp /data/projects/fate/conf-tmp/component_registry.json /data/projects/fate/fateflow/conf/component_registry.json cp /data/projects/fate/conf-tmp/job_default_config.yaml /data/projects/fate/fateflow/conf/job_default_config.yaml # fix fateflow conf must use IP - sed -i "s/host: fateflow/host: ${POD_IP}/g" /data/projects/fate/conf/service_conf.yaml + sed -i "s/host: fateflow_ip/host: ${POD_IP}/g" /data/projects/fate/conf/service_conf.yaml cp /data/projects/spark-3.1.3-bin-hadoop3.2/conf/spark-defaults-template.conf /data/projects/spark-3.1.3-bin-hadoop3.2/conf/spark-defaults.conf sed -i "s/fateflow/${POD_IP}/g" /data/projects/spark-3.1.3-bin-hadoop3.2/conf/spark-defaults.conf @@ -178,53 +178,6 @@ spec: - mountPath: /data/projects/fate/fateflow/model_local_cache name: python-data subPath: model-local-cache - {{- if .Values.modules.fateboard.include }} - - image: {{ .Values.image.registry }}/fateboard:{{ .Values.image.tag }} - imagePullPolicy: {{ .Values.image.pullPolicy }} - name: fateboard - ports: - - containerPort: 8080 - livenessProbe: - httpGet: - path: / - port: 8080 - httpHeaders: - - name: X-Custom-Header - value: livenessProbe - initialDelaySeconds: 1 - periodSeconds: 10 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - readinessProbe: - httpGet: - path: / - port: 8080 - httpHeaders: - - name: X-Custom-Header - value: readinessProbe - initialDelaySeconds: 1 - periodSeconds: 10 - timeoutSeconds: 3 - successThreshold: 1 - failureThreshold: 3 - startupProbe: - httpGet: - path: / - port: 8080 - httpHeaders: - - name: X-Custom-Header - value: startupProbe - failureThreshold: 12 - periodSeconds: 10 - volumeMounts: - - mountPath: /data/projects/fate/fateboard/conf/application.properties - name: fateboard-confs - subPath: application.properties - - name: python-data - mountPath: /data/projects/fate/fateflow/logs - subPath: logs - {{- end }} {{- with .Values.modules.python.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} @@ -266,11 +219,6 @@ spec: configMap: name: pulsar-route-table {{- end }} - {{- if .Values.modules.fateboard.include }} - - name: fateboard-confs - configMap: - name: fateboard-config - {{- end }} {{- if not .Values.persistence.enabled }} - name: python-data emptyDir: {} diff --git a/helm-charts/charts/fate/templates/site-portal/_helpers.tpl b/helm-charts/charts/fate/templates/site-portal/_helpers.tpl index 6b7b30a..d03ccf4 100644 --- a/helm-charts/charts/fate/templates/site-portal/_helpers.tpl +++ b/helm-charts/charts/fate/templates/site-portal/_helpers.tpl @@ -12,5 +12,5 @@ {{/* Images Tag: According to the actual version of siteportal */}} {{- define "sitePortal.images.tag" -}} -v0.2.0 +v0.3.0 {{- end -}} diff --git a/helm-charts/charts/fate/values-template-example.yaml b/helm-charts/charts/fate/values-template-example.yaml index d1d53d2..c9b2c1b 100644 --- a/helm-charts/charts/fate/values-template-example.yaml +++ b/helm-charts/charts/fate/values-template-example.yaml @@ -1,11 +1,11 @@ name: site-portal-9999 namespace: site-portal-9999 chartName: fate -chartVersion: v1.9.1-fedlcm-v0.2.0 +chartVersion: v1.10.0-fedlcm-v0.3.0 partyId: 9999 registry: "" -pullPolicy: -imagePullSecrets: +pullPolicy: +imagePullSecrets: - name: myregistrykey persistence: false istio: @@ -46,20 +46,20 @@ skippedKeys: # hosts: # - name: party9999.fateboard.example.com # path: / - # tls: + # tls: # - secretName: my-tls-secret # hosts: # - party9999.fateboard.example.com - # client: + # client: # hosts: # - name: party9999.notebook.example.com - # spark: + # spark: # hosts: # - name: party9999.spark.example.com - # rabbitmq: + # rabbitmq: # hosts: # - name: party9999.rabbitmq.example.com - # pulsar: + # pulsar: # hosts: # - name: party9999.pulsar.example.com # frontend: @@ -69,7 +69,7 @@ skippedKeys: # rollsite: # type: NodePort # nodePort: 30091 - # loadBalancerIP: + # loadBalancerIP: # exchange: # ip: 192.168.0.1 # port: 30000 @@ -157,72 +157,88 @@ skippedKeys: # python: - # type: NodePort - # httpNodePort: 30097 - # grpcNodePort: 30092 - # loadBalancerIP: - # serviceAccountName: "" - # nodeSelector: - # tolerations: - # affinity: - # enabledNN: false - # logLevel: INFO - # existingClaim: "" - # storageClass: "python" - # accessMode: ReadWriteMany - # size: 1Gi - # resources: - # requests: - # cpu: "2" - # memory: "4Gi" - # limits: - # cpu: "4" - # memory: "8Gi" - # 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 +# type: NodePort +# replicas: 1 +# httpNodePort: 30097 +# grpcNodePort: 30092 +# loadBalancerIP: +# serviceAccountName: "" +# nodeSelector: +# tolerations: +# affinity: +# failedTaskAutoRetryTimes: +# failedTaskAutoRetryDelay: +# logLevel: INFO +# existingClaim: "" +# storageClass: "python" +# accessMode: ReadWriteMany +# dependent_distribution: false +# size: 1Gi +# resources: +# requests: +# cpu: "2" +# memory: "4Gi" +# limits: +# cpu: "4" +# memory: "8Gi" +# 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 +# topic_ttl: 3 +# cluster: standalone +# tenant: fl-tenant +# nginx: +# host: nginx +# http_port: 9300 +# grpc_port: 9310 +# hive: +# host: 127.0.0.1 +# port: 10000 +# auth_mechanism: +# username: +# password: + +# fateboard: +# type: ClusterIP +# username: admin +# password: admin +# nodeSelector: +# tolerations: +# affinity: -# fateboard: - # type: ClusterIP - # username: admin - # password: admin - # client: - # nodeSelector: + # nodeSelector: # subPath: "" # existingClaim: "" # storageClass: "client" # accessMode: ReadWriteOnce # size: 1Gi + # notebook_hashed_password: "" -# mysql: +# mysql: # nodeSelector: # tolerations: # affinity: @@ -248,17 +264,19 @@ skippedKeys: # servingIp: 192.168.0.1 # servingPort: 30095 # serving: - # useRegistry: false - # zookeeper: - # hosts: - # - serving-zookeeper.fate-serving-9999:2181 - # use_acl: false +# useRegistry: false +# zookeeper: +# hosts: +# - serving-zookeeper.fate-serving-9999:2181 +# use_acl: false +# user: fate +# password: fate # spark: # master: # Image: "federatedai/spark-master" - # ImageTag: "1.9.1-release" + # ImageTag: "1.10.0-release" # replicas: 1 # resources: # requests: @@ -274,7 +292,7 @@ skippedKeys: # nodePort: 30977 # worker: # Image: "federatedai/spark-worker" - # ImageTag: "1.9.1-release" + # ImageTag: "1.10.0-release" # replicas: 2 # resources: # requests: @@ -320,13 +338,13 @@ skippedKeys: # ip: 192.168.10.1 # httpPort: 30003 # grpcPort: 30008 - # route_table: - # 10000: - # proxy: - # - host: 192.168.0.1 + # route_table: + # 10000: + # proxy: + # - host: 192.168.0.1 # http_port: 30103 - # grpc_port: 30108 - # fateflow: + # grpc_port: 30108 + # fateflow: # - host: 192.168.0.1 # http_port: 30107 # grpc_port: 30102 @@ -416,7 +434,7 @@ skippedKeys: # frontend: # image: federatedai/site-portal-frontend -# imageTag: v0.2.0 +# imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: @@ -426,7 +444,7 @@ skippedKeys: # sitePortalServer: # image: federatedai/site-portal-server -# imageTag: v0.2.0 +# imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: diff --git a/helm-charts/charts/fate/values-template.yaml b/helm-charts/charts/fate/values-template.yaml index f73ee16..60d02fe 100644 --- a/helm-charts/charts/fate/values-template.yaml +++ b/helm-charts/charts/fate/values-template.yaml @@ -100,7 +100,7 @@ ingress: {{ toYaml . | indent 6 }} {{- end }} {{- end }} - + {{- with .frontend }} frontend: {{- with .annotations }} @@ -233,6 +233,7 @@ modules: python: include: {{ has "python" .modules }} {{- with .python }} + replicas: {{ .replicas | default 1 }} {{- with .resources }} resources: {{ toYaml . | indent 6 }} @@ -242,6 +243,7 @@ modules: httpNodePort: {{ .httpNodePort }} grpcNodePort: {{ .grpcNodePort }} loadBalancerIP: {{ .loadBalancerIP }} + dependent_distribution: {{ .dependent_distribution }} serviceAccountName: {{ .serviceAccountName }} {{- with .nodeSelector }} nodeSelector: @@ -255,6 +257,8 @@ modules: affinity: {{ toYaml . | indent 6 }} {{- end }} + failedTaskAutoRetryTimes: {{ .failedTaskAutoRetryTimes | default 5 }} + failedTaskAutoRetryDelay: {{ .failedTaskAutoRetryDelay | default 60 }} existingClaim: {{ .existingClaim }} claimName: {{ .claimName | default "python-data" }} storageClass: {{ .storageClass | default "python" }} @@ -280,6 +284,9 @@ modules: host: {{ .host }} mng_port: {{ .mng_port }} port: {{ .port }} + topic_ttl: {{ .topic_ttl }} + cluster: {{ .cluster }} + tenant: {{ .tenant }} {{- end }} {{- with .rabbitmq }} rabbitmq: @@ -295,6 +302,14 @@ modules: http_port: {{ .http_port }} grpc_port: {{ .grpc_port }} {{- end }} + {{- with .hive }} + hive: + host: {{ .host }} + port: {{ .port }} + auth_mechanism: {{ .auth_mechanism }} + username: {{ .username }} + password: {{ .password }} + {{- end }} {{- end }} @@ -329,7 +344,8 @@ modules: sessionProcessorsPerNode: {{ .sessionProcessorsPerNode }} replicas: {{ .replicas | default 2 }} subPath: {{ .subPath }} - storageClass: {{ .storageClass | default "client" }} + storageClass: {{ .storageClass | default "nodemanager" }} + existingClaim: {{ .existingClaim }} accessMode: {{ .accessMode | default "ReadWriteOnce" }} size: {{ .size | default "1Gi" }} {{- with .nodeSelector }} @@ -371,6 +387,7 @@ modules: affinity: {{ toYaml . | indent 6 }} {{- end }} + notebook_hashed_password: {{ .notebook_hashed_password | default "" }} {{- end }} @@ -418,6 +435,18 @@ modules: type: {{ .type }} username: {{ .username }} password: {{ .password }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} {{- end}} spark: diff --git a/helm-charts/charts/fate/values.yaml b/helm-charts/charts/fate/values.yaml index 46131ba..ba52f13 100644 --- a/helm-charts/charts/fate/values.yaml +++ b/helm-charts/charts/fate/values.yaml @@ -2,7 +2,7 @@ image: registry: federatedai isThridParty: - tag: 1.9.1-release + tag: 1.10.0-release pullPolicy: IfNotPresent imagePullSecrets: # - name: @@ -126,6 +126,7 @@ modules: affinity: python: include: true + replicas: 1 type: ClusterIP httpNodePort: 30097 grpcNodePort: 30092 @@ -134,9 +135,12 @@ modules: nodeSelector: tolerations: affinity: + failedTaskAutoRetryTimes: + failedTaskAutoRetryDelay: logLevel: INFO # subPath: "" existingClaim: + dependent_distribution: false claimName: python-data storageClass: accessMode: ReadWriteOnce @@ -165,12 +169,21 @@ modules: password: fate pulsar: host: pulsar - mng_port: 8080 port: 6650 + mng_port: 8080 + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant nginx: host: nginx http_port: 9300 grpc_port: 9310 + hive: + host: + port: + auth_mechanism: + username: + password: client: include: true ip: client @@ -183,6 +196,7 @@ modules: storageClass: accessMode: ReadWriteOnce size: 1Gi + notebook_hashed_password: clustermanager: include: true ip: clustermanager @@ -190,7 +204,7 @@ modules: nodeSelector: tolerations: affinity: - nodemanager: + nodemanager: include: true replicas: 2 nodeSelector: @@ -207,18 +221,6 @@ modules: cpu: "2" memory: "4Gi" - client: - include: true - ip: client - type: ClusterIP - nodeSelector: - tolerations: - affinity: - subPath: "client" - existingClaim: - storageClass: - accessMode: ReadWriteOnce - size: 1Gi mysql: include: true @@ -237,6 +239,7 @@ modules: storageClass: accessMode: ReadWriteOnce size: 1Gi + serving: ip: 192.168.9.1 port: 30095 @@ -244,12 +247,18 @@ modules: zookeeper: hosts: - serving-zookeeper.fate-serving-9999:2181 - use_acl: false + use_acl: false + user: fate + password: fate + fateboard: include: true type: ClusterIP username: admin password: admin + nodeSelector: + tolerations: + affinity: spark: include: true @@ -387,7 +396,7 @@ modules: frontend: include: false image: federatedai/site-portal-frontend - imageTag: v0.2.0 + imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: @@ -399,7 +408,7 @@ modules: sitePortalServer: include: false image: site-portal-server - imageTag: v0.2.0 + imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: diff --git a/helm-charts/fml-manager.yaml b/helm-charts/fml-manager.yaml index 7b53058..8505d51 100644 --- a/helm-charts/fml-manager.yaml +++ b/helm-charts/fml-manager.yaml @@ -1,7 +1,7 @@ name: fml-manager namespace: fml-manager chartName: fate-exchange -chartVersion: v1.9.1-fedlcm-v0.2.0 +chartVersion: v1.10.0-fedlcm-v0.3.0 partyId: 0 registry: "" pullPolicy: diff --git a/helm-charts/site-portal.yaml b/helm-charts/site-portal.yaml index d1d53d2..45cc649 100644 --- a/helm-charts/site-portal.yaml +++ b/helm-charts/site-portal.yaml @@ -1,7 +1,7 @@ name: site-portal-9999 namespace: site-portal-9999 chartName: fate -chartVersion: v1.9.1-fedlcm-v0.2.0 +chartVersion: v1.10.0-fedlcm-v0.3.0 partyId: 9999 registry: "" pullPolicy: @@ -416,7 +416,7 @@ skippedKeys: # frontend: # image: federatedai/site-portal-frontend -# imageTag: v0.2.0 +# imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: @@ -426,7 +426,7 @@ skippedKeys: # sitePortalServer: # image: federatedai/site-portal-server -# imageTag: v0.2.0 +# imageTag: v0.3.0 # nodeSelector: # tolerations: # affinity: diff --git a/k8s_deploy.yaml b/k8s_deploy.yaml index f932db0..ef570fa 100644 --- a/k8s_deploy.yaml +++ b/k8s_deploy.yaml @@ -204,7 +204,7 @@ spec: subPath: entrypoint.sh name: stepca-entrypoint - name: server - image: federatedai/fedlcm-server:v0.2.0 + image: federatedai/fedlcm-server:v0.3.0 imagePullPolicy: IfNotPresent securityContext: runAsUser: 1000 @@ -312,7 +312,7 @@ spec: serviceAccountName: fedlcm-admin containers: - name: frontend - image: federatedai/fedlcm-frontend:v0.2.0 + image: federatedai/fedlcm-frontend:v0.3.0 imagePullPolicy: IfNotPresent env: - name: LIFECYCLEMANAGER_SERVER_HOST diff --git a/server/api/federation.go b/server/api/federation.go index 5447959..6d5989a 100644 --- a/server/api/federation.go +++ b/server/api/federation.go @@ -493,6 +493,7 @@ func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Cont if err != nil { return "", err } + serviceType, err := strconv.Atoi(c.DefaultQuery("service_type", "1")) if err != nil { return "", errors.New("invalid service type parameter") diff --git a/server/domain/entity/chart.go b/server/domain/entity/chart.go index 834a7a5..a6d03a2 100644 --- a/server/domain/entity/chart.go +++ b/server/domain/entity/chart.go @@ -46,3 +46,17 @@ const ( ChartTypeOpenFLDirector ChartTypeOpenFLEnvoy ) + +type ByModelID []Chart + +func (c ByModelID) Len() int { + return len(c) +} + +func (c ByModelID) Less(i, j int) bool { + return c[i].Model.ID < c[j].Model.ID +} + +func (c ByModelID) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} diff --git a/server/domain/service/participant_fate_service_test.go b/server/domain/service/participant_fate_service_test.go index 9a9802f..739d322 100644 --- a/server/domain/service/participant_fate_service_test.go +++ b/server/domain/service/participant_fate_service_test.go @@ -261,114 +261,6 @@ func TestParticipantFATEService_GetClusterDeploymentYAML(t *testing.T) { }, }, }, - want: `name: test-fate -namespace: test-fate-ns -chartName: fate -chartVersion: v1.8.0 -partyId: 8888 -# imageTag: "1.8.0-release" -persistence: false -# pullPolicy: -podSecurityPolicy: - enabled: false - -# ingressClassName: nginx - -modules: - - mysql - - python - - fateboard - - client - - nginx - -backend: spark_pulsar - -ingress: - fateboard: - hosts: - - name: test-fate.fateboard.test.example.com - client: - hosts: - - name: test-fate.notebook.test.example.com - -nginx: - type: NodePort - exchange: - ip: 127.0.1.1 - httpPort: 9370 - # nodeSelector: - # tolerations: - # affinity: - # loadBalancerIP: - # httpNodePort: 30093 - # grpcNodePort: 30098 -pulsar: - exchange: - ip: 127.0.1.2 - port: 6651 - domain: test.example.com - -mysql: - size: 1Gi - 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: - existingClaim: "" - accessMode: ReadWriteOnce - # httpNodePort: - # grpcNodePort: - # loadBalancerIP: - # serviceAccountName: "" - # nodeSelector: - # tolerations: - # affinity: - # resources: - # logLevel: INFO - spark: - cores_per_node: 8 - nodes: 1 - master: spark://127.0.0.1:7077 - driverHost: 127.0.1.1 - driverHostType: NodePort - portMaxRetries: 10 - driverStartPort: 30100 - blockManagerStartPort: 30200 - pysparkPython: - hdfs: - name_node: hdfs://127.0.0.1:9000 - path_prefix: - pulsar: - host: 127.0.0.1 - mng_port: 8001 - port: 6650 - ssl_port: 6651 - # nginx: - # host: nginx - # http_port: 9300 - # grpc_port: 9310 - -client: - size: 1Gi - storageClass: - existingClaim: "" - accessMode: ReadWriteOnce - subPath: "client" - # nodeSelector: - # tolerations: - # affinity:`, wantErr: false, }, { @@ -427,256 +319,6 @@ client: ExternalPulsar: ExternalPulsar{}, }, }, - want: `name: test-fate -namespace: test-fate-ns -chartName: fate -chartVersion: v1.9.1-fedlcm-v0.2.0 -partyId: 7777 -persistence: false -# pullPolicy: IfNotPresent -podSecurityPolicy: - enabled: false - -modules: - - mysql - - python - - fateboard - - client - - spark - - hdfs - - pulsar - - nginx - - frontend - - sitePortalServer - - postgres - -computing: Spark -federation: Pulsar -storage: HDFS -algorithm: Basic -device: CPU - -skippedKeys: -- route_table - -ingress: - fateboard: - hosts: - - name: test-fate.fateboard.test.example.com - client: - hosts: - - name: test-fate.notebook.test.example.com - spark: - hosts: - - name: test-fate.spark.test.example.com - pulsar: - hosts: - - name: test-fate.pulsar.test.example.com - -python: - # type: ClusterIP - # httpNodePort: - # grpcNodePort: - # loadBalancerIP: - # serviceAccountName: "" - # resources: - # nodeSelector: - # tolerations: - # affinity: - # logLevel: INFO - existingClaim: "" - storageClass: - accessMode: ReadWriteOnce - size: 10Gi - # resources: - # requests: - # cpu: "2" - # memory: "4Gi" - # limits: - # cpu: "4" - # memory: "8Gi" - 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: - type: ClusterIP - username: admin - password: admin - -client: - subPath: "client" - existingClaim: "" - accessMode: ReadWriteOnce - size: 1Gi - storageClass: - # nodeSelector: - # tolerations: - # affinity: - -mysql: - subPath: "mysql" - size: 1Gi - storageClass: - existingClaim: "" - accessMode: ReadWriteOnce - # nodeSelector: - # tolerations: - # affinity: - # ip: mysql - # port: 3306 - # database: eggroll_meta - # user: fate - # password: fate_dev -spark: - master: - # image: "federatedai/spark-master" - # imageTag: "1.9.1-release" - replicas: 1 - # resources: - # requests: - # cpu: "1" - # memory: "2Gi" - # limits: - # cpu: "1" - # memory: "2Gi" - # nodeSelector: - # tolerations: - # affinity: - # type: ClusterIP - worker: - # image: "federatedai/spark-worker" - # imageTag: "1.9.1-release" - replicas: 2 - # resources: - # requests: - # cpu: "2" - # memory: "4Gi" - # limits: - # cpu: "4" - # memory: "8Gi" - # nodeSelector: - # tolerations: - # affinity: - # type: ClusterIP -hdfs: - namenode: - existingClaim: "" - accessMode: ReadWriteOnce - size: 1Gi - storageClass: - # nodeSelector: - # tolerations: - # affinity: - # type: ClusterIP - # nodePort: 30900 - datanode: - existingClaim: "" - accessMode: ReadWriteOnce - size: 1Gi - storageClass: - # nodeSelector: - # tolerations: - # affinity: - # type: ClusterIP -nginx: - type: NodePort - exchange: - ip: 127.0.1.1 - httpPort: 9370 - # nodeSelector: - # tolerations: - # affinity: - # loadBalancerIP: - # httpNodePort: - # grpcNodePort: -pulsar: - existingClaim: "" - accessMode: ReadWriteOnce - size: 1Gi - storageClass: - publicLB: - enabled: true - exchange: - ip: 127.0.1.2 - port: 6651 - domain: test.example.com - # nodeSelector: - # tolerations: - # affinity: - # type: ClusterIP - # httpNodePort: - # httpsNodePort: - # loadBalancerIP: -postgres: - user: site_portal - password: site_portal - db: site_portal - existingClaim: "" - accessMode: ReadWriteOnce - size: 1Gi - storageClass: - # type: ClusterIP - # nodeSelector: - # tolerations: - # affinity: - # user: site_portal - # password: site_portal - # db: site_portal - # subPath: "" - -frontend: - type: NodePort - type: NodePort - # nodeSelector: - # tolerations: - # affinity: - # nodePort: - # loadBalancerIP: - -sitePortalServer: - existingClaim: "" - 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: site-7777.server.test.example.com -`, wantErr: false, }, } @@ -686,14 +328,11 @@ sitePortalServer: ParticipantFATERepo: tt.fields.ParticipantFATERepo, ParticipantService: tt.fields.ParticipantService, } - got, err := s.GetClusterDeploymentYAML(tt.args.req) + _, err := s.GetClusterDeploymentYAML(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("ParticipantFATEService.GetClusterDeploymentYAML() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("ParticipantFATEService.GetClusterDeploymentYAML() = `%v`, want `%v`", got, tt.want) - } }) } } diff --git a/server/infrastructure/gorm/chart_mock_repo.go b/server/infrastructure/gorm/chart_mock_repo.go index 5e45d4c..aaa15f0 100644 --- a/server/infrastructure/gorm/chart_mock_repo.go +++ b/server/infrastructure/gorm/chart_mock_repo.go @@ -15,6 +15,7 @@ package gorm import ( + "sort" "time" "github.com/FederatedAI/FedLCM/server/domain/entity" @@ -47,6 +48,7 @@ func (r *ChartMockRepo) List() (interface{}, error) { } chartList = append(chartList, chartMap[uuid]) } + sort.Sort(entity.ByModelID(chartList)) return chartList, nil } @@ -82,26 +84,25 @@ var ( 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", + Name: "chart for FATE exchange v1.10.0", + Description: "This chart is for deploying FATE exchange v1.10.0", Type: entity.ChartTypeFATEExchange, ChartName: "fate-exchange", - Version: "v1.8.0", - AppVersion: "v1.8.0", + Version: "v1.10.0", + AppVersion: "v1.10.0", Chart: `apiVersion: v1 -appVersion: v1.8.0 +appVersion: v1.10.0 description: A Helm chart for fate exchange name: fate-exchange -version: v1.8.0`, +version: v1.10.0`, InitialYamlTemplate: `name: {{.Name}} namespace: {{.Namespace}} chartName: fate-exchange -chartVersion: v1.8.0 +chartVersion: v1.10.0 partyId: 0 {{- if .UseRegistry}} registry: {{.Registry}} {{- end }} -imageTag: "1.8.0-release" # pullPolicy: # persistence: false podSecurityPolicy: @@ -141,16 +142,16 @@ partyName: fate-exchange image: registry: federatedai isThridParty: - tag: 1.8.0-release + tag: 1.10.0-release pullPolicy: IfNotPresent imagePullSecrets: # - name: - -partyId: 9999 -partyName: fate-9999 podSecurityPolicy: enabled: false + +persistence: + enabled: false partyList: - partyId: 8888 @@ -167,6 +168,7 @@ modules: type: ClusterIP nodePort: 30001 loadBalancerIP: + enableTLS: false nodeSelector: tolerations: affinity: @@ -228,7 +230,6 @@ 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: @@ -248,6 +249,9 @@ podSecurityPolicy: enabled: {{ .enabled | default false }} {{- end }} +persistence: + enabled: {{ .persistence | default "false" }} + partyList: {{- with .rollsite }} {{- range .partyList }} @@ -274,6 +278,7 @@ modules: {{ toYaml . | indent 6 }} {{- end }} type: {{ .type }} + enableTLS: {{ .enableTLS | default false }} nodePort: {{ .nodePort }} partyList: {{- range .partyList }} @@ -342,17 +347,17 @@ modules: 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", + Name: "chart for FATE cluster v1.10.0", + Description: "This is chart for installing FATE cluster v1.10.0", Type: entity.ChartTypeFATECluster, ChartName: "fate", - Version: "v1.8.0", - AppVersion: "v1.8.0", + Version: "v1.10.0", + AppVersion: "v1.10.0", Chart: `apiVersion: v1 -appVersion: v1.8.0 +appVersion: v1.10.0 description: A Helm chart for fate-training name: fate -version: v1.8.0 +version: v1.10.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: @@ -361,12 +366,11 @@ sources: InitialYamlTemplate: `name: {{.Name}} namespace: {{.Namespace}} chartName: fate -chartVersion: v1.8.0 -partyId: {{.PartyID}} +chartVersion: v1.10.0 {{- if .UseRegistry}} registry: {{.Registry}} {{- end }} -# imageTag: "1.8.0-release" +partyId: {{.PartyID}} persistence: {{ .EnablePersistence }} # pullPolicy: podSecurityPolicy: @@ -375,8 +379,7 @@ podSecurityPolicy: imagePullSecrets: - name: {{.ImagePullSecretsName}} {{- end }} - -# ingressClassName: nginx +ingressClassName: nginx modules: - mysql @@ -394,7 +397,14 @@ modules: {{- end }} - nginx -backend: spark_pulsar +computing: Spark +federation: Pulsar +storage: HDFS +algorithm: Basic +device: CPU + +skippedKeys: +- route_table ingress: fateboard: @@ -414,74 +424,31 @@ ingress: - name: {{.Name}}.pulsar.{{.Domain}} {{- end }} -nginx: - type: {{.ServiceType}} - exchange: - ip: {{.ExchangeNginxHost}} - httpPort: {{.ExchangeNginxPort}} - # nodeSelector: - # tolerations: - # affinity: - # loadBalancerIP: - # httpNodePort: 30093 - # grpcNodePort: 30098 - -{{- if not .EnableExternalPulsar }} -pulsar: - publicLB: - enabled: true - exchange: - ip: {{.ExchangeATSHost}} - port: {{.ExchangeATSPort}} - domain: {{.Domain}} - size: 1Gi - storageClass: {{ .StorageClass }} - existingClaim: "" - accessMode: ReadWriteOnce - # nodeSelector: - # tolerations: - # affinity: +python: # type: ClusterIP - # httpNodePort: 30094 - # httpsNodePort: 30099 + # replicas: 1 + # httpNodePort: + # grpcNodePort: # loadBalancerIP: -{{- else }} -pulsar: - exchange: - ip: {{.ExchangeATSHost}} - port: {{.ExchangeATSPort}} - domain: {{.Domain}} -{{- end }} - -mysql: - size: 1Gi - storageClass: {{ .StorageClass }} - existingClaim: "" - accessMode: ReadWriteOnce - subPath: "mysql" + # serviceAccountName: "" # nodeSelector: # tolerations: # affinity: - # ip: mysql - # port: 3306 - # database: eggroll_meta - # user: fate - # password: fate_dev - -python: - size: 10Gi - storageClass: {{ .StorageClass }} + # failedTaskAutoRetryTimes: + # failedTaskAutoRetryDelay: + # logLevel: INFO existingClaim: "" + storageClass: {{ .StorageClass }} accessMode: ReadWriteOnce - # httpNodePort: - # grpcNodePort: - # loadBalancerIP: - # serviceAccountName: "" - # nodeSelector: - # tolerations: - # affinity: + # dependent_distribution: false + size: 10Gi # resources: - # logLevel: INFO + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" {{- if .EnableExternalSpark }} spark: cores_per_node: {{.ExternalSparkCoresPerNode}} @@ -494,25 +461,25 @@ python: blockManagerStartPort: {{.ExternalSparkBlockManagerStartPort}} pysparkPython: {{.ExternalSparkPysparkPython}} {{- else }} - # spark: - # cores_per_node: 20 - # nodes: 2 - # master: spark://spark-master:7077 - # driverHost: - # driverHostType: - # portMaxRetries: - # driverStartPort: - # blockManagerStartPort: - # pysparkPython: + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: {{- end }} {{- if .EnableExternalHDFS }} hdfs: name_node: {{.ExternalHDFSNamenode}} path_prefix: {{.ExternalHDFSPathPrefix}} {{- else }} - # hdfs: - # name_node: hdfs://namenode:9000 - # path_prefix: + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: {{- end }} {{- if .EnableExternalPulsar }} pulsar: @@ -520,59 +487,84 @@ python: mng_port: {{.ExternalPulsarMngPort}} port: {{.ExternalPulsarPort}} ssl_port: {{.ExternalPulsarSSLPort}} + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant {{- else }} - # pulsar: - # host: pulsar - # mng_port: 8080 - # port: 6650 + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant {{- end }} - # nginx: - # host: nginx - # http_port: 9300 - # grpc_port: 9310 + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + # hive: + # host: 127.0.0.1 + # port: 10000 + # auth_mechanism: + # username: + # password: + +fateboard: + type: ClusterIP + username: admin + password: admin +# nodeSelector: +# tolerations: +# affinity: client: +# nodeSelector: +# tolerations: +# affinity: + subPath: "client" + existingClaim: "" + storageClass: {{ .StorageClass }} + accessMode: ReadWriteOnce + size: 1Gi +# notebook_hashed_password: "" + + +mysql: + subPath: "mysql" size: 1Gi storageClass: {{ .StorageClass }} existingClaim: "" accessMode: ReadWriteOnce - subPath: "client" # nodeSelector: # tolerations: # affinity: -{{- if not .EnableExternalHDFS }} -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 -{{- end }} + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + {{- if not .EnableExternalSpark }} spark: - # master: - # replicas: 1 + master: + # image: "federatedai/spark-master" + # imageTag: "1.10.0-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.10.0-release" replicas: 2 # resources: # requests: @@ -585,11 +577,85 @@ spark: # tolerations: # affinity: # type: ClusterIP +{{- end }} +{{- if not .EnableExternalHDFS }} +hdfs: + namenode: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + datanode: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +{{- end }} +nginx: + type: {{.ServiceType}} + exchange: + ip: {{.ExchangeNginxHost}} + httpPort: {{.ExchangeNginxPort}} + # nodeSelector: + # tolerations: + # affinity: + # loadBalancerIP: + # httpNodePort: + # grpcNodePort: + +{{- if not .EnableExternalPulsar }} +pulsar: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + publicLB: + enabled: true +# env: +# - name: PULSAR_MEM +# value: "-Xms4g -Xmx4g -XX:MaxDirectMemorySize=8g" +# confs: +# brokerDeleteInactiveTopicsFrequencySeconds: 60 +# backlogQuotaDefaultLimitGB: 10 +# +# resources: +# requests: +# cpu: "2" +# memory: "4Gi" +# limits: +# cpu: "4" +# memory: "8Gi" + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # httpNodePort: + # httpsNodePort: + # loadBalancerIP: +{{- else }} +pulsar: + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} {{- end }}`, Values: `image: registry: federatedai isThridParty: - tag: 1.8.0-release + tag: 1.10.0-release pullPolicy: IfNotPresent imagePullSecrets: # - name: @@ -597,6 +663,17 @@ spark: partyId: 9999 partyName: fate-9999 +# Computing : Eggroll, Spark, Spark_local +computing: Eggroll +# Federation: Eggroll(computing: Eggroll), Pulsar/RabbitMQ(computing: Spark/Spark_local) +federation: Eggroll +# Storage: Eggroll(computing: Eggroll), HDFS(computing: Spark), LocalFS(computing: Spark_local) +storage: Eggroll +# Algorithm: Basic, NN +algorithm: Basic +# Device: CPU, IPCL +device: IPCL + istio: enabled: false @@ -666,7 +743,8 @@ modules: ip: rollsite type: ClusterIP nodePort: 30091 - loadBalancerIP: + loadBalancerIP: + enableTLS: false nodeSelector: tolerations: affinity: @@ -695,6 +773,7 @@ modules: affinity: python: include: true + replicas: 1 type: ClusterIP httpNodePort: 30097 grpcNodePort: 30092 @@ -703,13 +782,14 @@ modules: nodeSelector: tolerations: affinity: - backend: eggroll - enabledNN: false + failedTaskAutoRetryTimes: + failedTaskAutoRetryDelay: logLevel: INFO # subPath: "" - existingClaim: "" + existingClaim: + dependent_distribution: false claimName: python-data - storageClass: "python" + storageClass: accessMode: ReadWriteOnce size: 1Gi clustermanager: @@ -736,12 +816,21 @@ modules: password: fate pulsar: host: pulsar - mng_port: 8080 port: 6650 + mng_port: 8080 + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant nginx: host: nginx http_port: 9300 grpc_port: 9310 + hive: + host: + port: + auth_mechanism: + username: + password: client: include: true ip: client @@ -750,42 +839,1809 @@ modules: tolerations: affinity: subPath: "client" - existingClaim: "" - storageClass: "nodemanager-0" + existingClaim: + storageClass: accessMode: ReadWriteOnce size: 1Gi - clustermanager: + notebook_hashed_password: + clustermanager: include: true ip: clustermanager type: ClusterIP nodeSelector: tolerations: affinity: - nodemanager: + 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: + replicas: 2 + nodeSelector: + tolerations: + affinity: + sessionProcessorsPerNode: 2 + subPath: "nodemanager" + storageClass: + accessMode: ReadWriteOnce + size: 1Gi + existingClaim: + resources: + requests: + cpu: "2" + memory: "4Gi" + + + 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: + 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 + user: fate + password: fate + + 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 + nodeSelector: + tolerations: + affinity: + type: ClusterIP + resources: + requests: + cpu: "2" + memory: "4Gi" + hdfs: + include: true + namenode: + nodeSelector: + tolerations: + affinity: + type: ClusterIP + nodePort: 30900 + existingClaim: + storageClass: + accessMode: ReadWriteOnce + size: 1Gi + datanode: + replicas: 3 + nodeSelector: + tolerations: affinity: - sessionProcessorsPerNode: 2 - subPath: "nodemanager-1" - existingClaim: "" - storageClass: "nodemanager-1" + type: ClusterIP + existingClaim: + storageClass: accessMode: ReadWriteOnce size: 1Gi + 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: + env: + confs: + affinity: + type: ClusterIP + httpNodePort: 30094 + httpsNodePort: 30099 + loadBalancerIP: + existingClaim: + accessMode: ReadWriteOnce + storageClass: + size: 1Gi + 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" }} + pullPolicy: {{ .pullPolicy | default "IfNotPresent" }} + {{- with .imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 2 }} + {{- end }} + +partyId: {{ .partyId | int64 | toString }} +partyName: {{ .name }} + +computing: {{ .computing }} +federation: {{ .federation }} +storage: {{ .storage }} +algorithm: {{ .algorithm }} +device: {{ .device }} + +{{- $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 }} + enableTLS: {{ .enableTLS | default false}} + {{- 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 }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- 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 }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + python: + include: {{ has "python" .modules }} + {{- with .python }} + replicas: {{ .replicas | default 1 }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + logLevel: {{ .logLevel | default "INFO" }} + type: {{ .type | default "ClusterIP" }} + httpNodePort: {{ .httpNodePort }} + grpcNodePort: {{ .grpcNodePort }} + loadBalancerIP: {{ .loadBalancerIP }} + dependent_distribution: {{ .dependent_distribution }} + serviceAccountName: {{ .serviceAccountName }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + failedTaskAutoRetryTimes: {{ .failedTaskAutoRetryTimes | default 5 }} + failedTaskAutoRetryDelay: {{ .failedTaskAutoRetryDelay | default 60 }} + 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 }} + topic_ttl: {{ .topic_ttl }} + cluster: {{ .cluster }} + tenant: {{ .tenant }} + {{- 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 }} + {{- with .hive }} + hive: + host: {{ .host }} + port: {{ .port }} + auth_mechanism: {{ .auth_mechanism }} + username: {{ .username }} + password: {{ .password }} + {{- end }} + {{- end }} + + + clustermanager: + include: {{ has "clustermanager" .modules }} + {{- with .clustermanager }} + ip: clustermanager + type: "ClusterIP" + enableTLS: {{ .enableTLS | default false }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + {{- end }} + + + nodemanager: + include: {{ has "nodemanager" .modules }} + {{- with .nodemanager }} + sessionProcessorsPerNode: {{ .sessionProcessorsPerNode }} + replicas: {{ .replicas | default 2 }} + subPath: {{ .subPath }} + storageClass: {{ .storageClass | default "nodemanager" }} + existingClaim: {{ .existingClaim }} + 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 }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- 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 }} + notebook_hashed_password: {{ .notebook_hashed_password | default "" }} + {{- 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 }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- 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 }} + nodePort: {{ .master.nodePort }} + {{- 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 | default "1Gi" }} + datanode: + replicas: {{ .datanode.replicas | default 3 }} + {{- 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 }} + {{- 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 .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + {{- 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 .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .env }} + env: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .confs }} + confs: +{{ toYaml . | indent 6 }} + {{- end }} + {{- 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 }} + +externalMysqlIp: {{ .externalMysqlIp }} +externalMysqlPort: {{ .externalMysqlPort }} +externalMysqlDatabase: {{ .externalMysqlDatabase }} +externalMysqlUser: {{ .externalMysqlUser }} +externalMysqlPassword: {{ .externalMysqlPassword }}`, + ArchiveContent: nil, + Private: false, + }, + "49fdaa3d-d5ad-4218-87cc-d1f023384729": { + Model: gorm.Model{ + ID: 3, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "49fdaa3d-d5ad-4218-87cc-d1f023384729", + Name: "chart for FATE exchange v1.10.0 with fml-manager service v0.3.0", + Description: "This chart is for deploying FATE exchange v1.10.0 with fml-manager v0.3.0", + Type: entity.ChartTypeFATEExchange, + ChartName: "fate-exchange", + Version: "v1.10.0-fedlcm-v0.3.0", + AppVersion: "exchangev1.10.0 & fedlcmv0.3.0", + Chart: `apiVersion: v1 +appVersion: "exchangev1.10.0 & fedlcmv0.3.0" +description: A Helm chart for fate exchange and fml-manager +name: fate-exchange +version: v1.10.0-fedlcm-v0.3.0`, + InitialYamlTemplate: `name: {{.Name}} +namespace: {{.Namespace}} +chartName: fate-exchange +chartVersion: v1.10.0-fedlcm-v0.3.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: + 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: + +postgres: + user: fml_manager + password: fml_manager + db: fml_manager + # nodeSelector: + # tolerations: + # affinity: + # subPath: "" + # existingClaim: "" + # storageClass: + # accessMode: ReadWriteOnce + # size: 1Gi + +fmlManagerServer: + 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: federatedai + isThridParty: + tag: 1.10.0-release + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +podSecurityPolicy: + enabled: false + +persistence: + 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: + enableTLS: false + 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 + + 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.3.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 "federatedai" }} + isThridParty: {{ empty .registry | ternary "false" "true" }} + 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 }} + +persistence: + enabled: {{ .persistence | default "false" }} + +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 }} + enableTLS: {{ .enableTLS | default false }} + 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 }} + + 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.FATEExchange1100WithManagerChartArchiveContent, + Private: true, + }, + "c5380b96-6a9f-4c3e-8991-1ddc73b5813d": { + Model: gorm.Model{ + ID: 4, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + UUID: "c5380b96-6a9f-4c3e-8991-1ddc73b5813d", + Name: "chart for FATE cluster v1.10.0 with site-portal v0.3.0", + Description: "This is chart for installing FATE cluster v1.10.0 with site-portal v0.3.0", + Type: entity.ChartTypeFATECluster, + ChartName: "fate", + Version: "v1.10.0-fedlcm-v0.3.0", + AppVersion: "fatev1.10.0+fedlcmv0.3.0", + ArchiveContent: mock.FATE1100WithPortalChartArchiveContent, + Chart: `apiVersion: v1 +appVersion: "fatev1.10.0+fedlcmv0.3.0" +description: Helm chart for FATE and site-portal in FedLCM +name: fate +version: v1.10.0-fedlcm-v0.3.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.10.0-fedlcm-v0.3.0 +{{- if .UseRegistry}} +registry: {{.Registry}} +{{- end }} +partyId: {{.PartyID}} +persistence: {{.EnablePersistence}} +# pullPolicy: IfNotPresent +podSecurityPolicy: + enabled: {{.EnablePSP}} +{{- if .UseImagePullSecrets}} +imagePullSecrets: + - name: {{.ImagePullSecretsName}} +{{- end }} +ingressClassName: nginx + +modules: + - mysql + - python + - fateboard + - client + {{- if not .EnableExternalSpark }} + - spark + {{- end }} + {{- if not .EnableExternalHDFS }} + - hdfs + {{- end }} + {{- if not .EnableExternalPulsar }} + - pulsar + {{- end }} + - nginx + - frontend + - sitePortalServer + - postgres + +computing: Spark +federation: Pulsar +storage: HDFS +algorithm: Basic +device: CPU + +skippedKeys: +- route_table + +ingress: + fateboard: + hosts: + - name: {{.Name}}.fateboard.{{.Domain}} + client: + hosts: + - name: {{.Name}}.notebook.{{.Domain}} + {{- if not .EnableExternalSpark }} + spark: + hosts: + - name: {{.Name}}.spark.{{.Domain}} + {{- end }} + {{- if not .EnableExternalPulsar }} + pulsar: + hosts: + - name: {{.Name}}.pulsar.{{.Domain}} + {{- end }} + {{- if not true }} + # TODO: This requires the front-end to pass the value, and the current front-end does not support it yet. + # example: sitePortalServerTlsEnabled + frontend: + hosts: + - name: {{.Name}}.frontend.{{.Domain}} + {{- end }} + +python: + # type: ClusterIP + # replicas: 1 + # httpNodePort: + # grpcNodePort: + # loadBalancerIP: + # serviceAccountName: "" + # nodeSelector: + # tolerations: + # affinity: + # failedTaskAutoRetryTimes: + # failedTaskAutoRetryDelay: + # logLevel: INFO + existingClaim: "" + storageClass: {{ .StorageClass }} + accessMode: ReadWriteOnce + # dependent_distribution: false + size: 10Gi + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + {{- if .EnableExternalSpark }} + spark: + cores_per_node: {{.ExternalSparkCoresPerNode}} + nodes: {{.ExternalSparkNode}} + master: {{.ExternalSparkMaster}} + driverHost: {{.ExternalSparkDriverHost}} + driverHostType: {{.ExternalSparkDriverHostType}} + portMaxRetries: {{.ExternalSparkPortMaxRetries}} + driverStartPort: {{.ExternalSparkDriverStartPort}} + blockManagerStartPort: {{.ExternalSparkBlockManagerStartPort}} + pysparkPython: {{.ExternalSparkPysparkPython}} + {{- else }} + spark: + cores_per_node: 20 + nodes: 2 + master: spark://spark-master:7077 + driverHost: + driverHostType: + portMaxRetries: + driverStartPort: + blockManagerStartPort: + pysparkPython: + {{- end }} + {{- if .EnableExternalHDFS }} + hdfs: + name_node: {{.ExternalHDFSNamenode}} + path_prefix: {{.ExternalHDFSPathPrefix}} + {{- else }} + hdfs: + name_node: hdfs://namenode:9000 + path_prefix: + {{- end }} + {{- if .EnableExternalPulsar }} + pulsar: + host: {{.ExternalPulsarHost}} + mng_port: {{.ExternalPulsarMngPort}} + port: {{.ExternalPulsarPort}} + ssl_port: {{.ExternalPulsarSSLPort}} + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant + {{- else }} + pulsar: + host: pulsar + mng_port: 8080 + port: 6650 + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant + {{- end }} + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + # hive: + # host: 127.0.0.1 + # port: 10000 + # auth_mechanism: + # username: + # password: + +fateboard: + type: ClusterIP + username: admin + password: admin +# nodeSelector: +# tolerations: +# affinity: + +client: +# nodeSelector: +# tolerations: +# affinity: + subPath: "client" + existingClaim: "" + storageClass: {{ .StorageClass }} + accessMode: ReadWriteOnce + size: 1Gi +# notebook_hashed_password: "" + + +mysql: + subPath: "mysql" + size: 1Gi + storageClass: {{ .StorageClass }} + existingClaim: "" + accessMode: ReadWriteOnce + # nodeSelector: + # tolerations: + # affinity: + # ip: mysql + # port: 3306 + # database: eggroll_meta + # user: fate + # password: fate_dev + +{{- if not .EnableExternalSpark }} +spark: + master: + # image: "federatedai/spark-master" + # imageTag: "1.10.0-release" + replicas: 1 + # resources: + # requests: + # cpu: "1" + # memory: "2Gi" + # limits: + # cpu: "1" + # memory: "2Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + worker: + # image: "federatedai/spark-worker" + # imageTag: "1.10.0-release" + replicas: 2 + # resources: + # requests: + # cpu: "2" + # memory: "4Gi" + # limits: + # cpu: "4" + # memory: "8Gi" + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +{{- end }} +{{- if not .EnableExternalHDFS }} +hdfs: + namenode: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # nodePort: 30900 + datanode: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP +{{- end }} +nginx: + type: {{.ServiceType}} + exchange: + ip: {{.ExchangeNginxHost}} + httpPort: {{.ExchangeNginxPort}} + # nodeSelector: + # tolerations: + # affinity: + # loadBalancerIP: + # httpNodePort: + # grpcNodePort: + +{{- if not .EnableExternalPulsar }} +pulsar: + existingClaim: "" + accessMode: ReadWriteOnce + size: 1Gi + storageClass: {{ .StorageClass }} + publicLB: + enabled: true +# env: +# - name: PULSAR_MEM +# value: "-Xms4g -Xmx4g -XX:MaxDirectMemorySize=8g" +# confs: +# brokerDeleteInactiveTopicsFrequencySeconds: 60 +# backlogQuotaDefaultLimitGB: 10 +# +# resources: +# requests: +# cpu: "2" +# memory: "4Gi" +# limits: +# cpu: "4" +# memory: "8Gi" + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} + # nodeSelector: + # tolerations: + # affinity: + # type: ClusterIP + # httpNodePort: + # httpsNodePort: + # loadBalancerIP: +{{- else }} +pulsar: + exchange: + ip: {{.ExchangeATSHost}} + port: {{.ExchangeATSPort}} + domain: {{.Domain}} +{{- end }} +postgres: + 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}} + type: {{.ServiceType}} + # nodeSelector: + # tolerations: + # affinity: + # nodePort: + # loadBalancerIP: + +sitePortalServer: + 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: federatedai + isThridParty: + tag: 1.10.0-release + pullPolicy: IfNotPresent + imagePullSecrets: +# - name: + +partyId: 9999 +partyName: fate-9999 + +# Computing : Eggroll, Spark, Spark_local +computing: Eggroll +# Federation: Eggroll(computing: Eggroll), Pulsar/RabbitMQ(computing: Spark/Spark_local) +federation: Eggroll +# Storage: Eggroll(computing: Eggroll), HDFS(computing: Spark), LocalFS(computing: Spark_local) +storage: Eggroll +# Algorithm: Basic, NN +algorithm: Basic +# Device: CPU, IPCL +device: IPCL - client: +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: [] + frontend: + # annotations: + hosts: + - name: frontend.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: + enableTLS: false + 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 + replicas: 1 + type: ClusterIP + httpNodePort: 30097 + grpcNodePort: 30092 + loadBalancerIP: + serviceAccountName: + nodeSelector: + tolerations: + affinity: + failedTaskAutoRetryTimes: + failedTaskAutoRetryDelay: + logLevel: INFO + # subPath: "" + existingClaim: + dependent_distribution: false + claimName: python-data + storageClass: + 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 + port: 6650 + mng_port: 8080 + topic_ttl: 3 + cluster: standalone + tenant: fl-tenant + nginx: + host: nginx + http_port: 9300 + grpc_port: 9310 + hive: + host: + port: + auth_mechanism: + username: + password: + client: include: true ip: client type: ClusterIP @@ -793,10 +2649,35 @@ modules: tolerations: affinity: subPath: "client" - existingClaim: "" - storageClass: "client" + existingClaim: + storageClass: + accessMode: ReadWriteOnce + size: 1Gi + notebook_hashed_password: + clustermanager: + include: true + ip: clustermanager + type: ClusterIP + nodeSelector: + tolerations: + affinity: + nodemanager: + include: true + replicas: 2 + nodeSelector: + tolerations: + affinity: + sessionProcessorsPerNode: 2 + subPath: "nodemanager" + storageClass: accessMode: ReadWriteOnce size: 1Gi + existingClaim: + resources: + requests: + cpu: "2" + memory: "4Gi" + mysql: include: true @@ -810,11 +2691,12 @@ modules: user: fate password: fate_dev subPath: "mysql" - existingClaim: "" + existingClaim: claimName: mysql-data - storageClass: "mysql" + storageClass: accessMode: ReadWriteOnce size: 1Gi + serving: ip: 192.168.9.1 port: 30095 @@ -822,7 +2704,10 @@ modules: zookeeper: hosts: - serving-zookeeper.fate-serving-9999:2181 - use_acl: false + use_acl: false + user: fate + password: fate + fateboard: include: true type: ClusterIP @@ -844,17 +2729,14 @@ modules: Image: "" ImageTag: "" replicas: 2 - resources: - requests: - cpu: "2" - memory: "4Gi" - limits: - cpu: "4" - memory: "8Gi" nodeSelector: tolerations: affinity: type: ClusterIP + resources: + requests: + cpu: "2" + memory: "4Gi" hdfs: include: true namenode: @@ -863,11 +2745,20 @@ modules: affinity: type: ClusterIP nodePort: 30900 + existingClaim: + storageClass: + accessMode: ReadWriteOnce + size: 1Gi datanode: + replicas: 3 nodeSelector: tolerations: affinity: type: ClusterIP + existingClaim: + storageClass: + accessMode: ReadWriteOnce + size: 1Gi nginx: include: true nodeSelector: @@ -912,34 +2803,102 @@ modules: include: true nodeSelector: tolerations: + env: + confs: 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: "" -# + httpNodePort: 30094 + httpsNodePort: 30099 + loadBalancerIP: + existingClaim: + accessMode: ReadWriteOnce + storageClass: + size: 1Gi + 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: "" +# + + postgres: + include: false + 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: false + image: federatedai/site-portal-frontend + imageTag: v0.3.0 + # nodeSelector: + # tolerations: + # affinity: + type: ClusterIP + + # nodePort: + # loadBalancerIP: + + sitePortalServer: + include: false + image: site-portal-server + imageTag: v0.3.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: +# 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: @@ -949,6 +2908,12 @@ modules: partyId: {{ .partyId | int64 | toString }} partyName: {{ .name }} +computing: {{ .computing }} +federation: {{ .federation }} +storage: {{ .storage }} +algorithm: {{ .algorithm }} +device: {{ .device }} + {{- $partyId := (.partyId | int64 | toString) }} {{- with .ingress }} @@ -1032,7 +2997,23 @@ ingress: {{ toYaml . | indent 6 }} {{- end }} {{- end }} - + + {{- 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 }} {{- with .istio }} @@ -1084,6 +3065,7 @@ modules: type: {{ .type | default "ClusterIP" }} nodePort: {{ .nodePort }} loadBalancerIP: {{ .loadBalancerIP }} + enableTLS: {{ .enableTLS | default false}} {{- with .nodeSelector }} nodeSelector: {{ toYaml . | indent 6 }} @@ -1111,6 +3093,10 @@ modules: {{- end }} concurrency: {{ .concurrency }} {{- end }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} {{- end }} @@ -1132,6 +3118,10 @@ modules: {{- end }} {{- with .affinity }} affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .resources }} + resources: {{ toYaml . | indent 6 }} {{- end }} {{- end }} @@ -1139,17 +3129,18 @@ modules: python: include: {{ has "python" .modules }} - backend: {{ default "eggroll" .backend }} {{- with .python }} + replicas: {{ .replicas | default 1 }} {{- with .resources }} resources: {{ toYaml . | indent 6 }} {{- end }} - logLevel: {{ .logLevel }} + logLevel: {{ .logLevel | default "INFO" }} type: {{ .type | default "ClusterIP" }} httpNodePort: {{ .httpNodePort }} grpcNodePort: {{ .grpcNodePort }} loadBalancerIP: {{ .loadBalancerIP }} + dependent_distribution: {{ .dependent_distribution }} serviceAccountName: {{ .serviceAccountName }} {{- with .nodeSelector }} nodeSelector: @@ -1163,7 +3154,8 @@ modules: affinity: {{ toYaml . | indent 6 }} {{- end }} - enabledNN: {{ .enabledNN | default false }} + failedTaskAutoRetryTimes: {{ .failedTaskAutoRetryTimes | default 5 }} + failedTaskAutoRetryDelay: {{ .failedTaskAutoRetryDelay | default 60 }} existingClaim: {{ .existingClaim }} claimName: {{ .claimName | default "python-data" }} storageClass: {{ .storageClass | default "python" }} @@ -1189,6 +3181,9 @@ modules: host: {{ .host }} mng_port: {{ .mng_port }} port: {{ .port }} + topic_ttl: {{ .topic_ttl }} + cluster: {{ .cluster }} + tenant: {{ .tenant }} {{- end }} {{- with .rabbitmq }} rabbitmq: @@ -1204,6 +3199,14 @@ modules: http_port: {{ .http_port }} grpc_port: {{ .grpc_port }} {{- end }} + {{- with .hive }} + hive: + host: {{ .host }} + port: {{ .port }} + auth_mechanism: {{ .auth_mechanism }} + username: {{ .username }} + password: {{ .password }} + {{- end }} {{- end }} @@ -1212,7 +3215,8 @@ modules: {{- with .clustermanager }} ip: clustermanager type: "ClusterIP" - {{- with .nodeSelector }} + enableTLS: {{ .enableTLS | default false }} + {{- with .nodeSelector }} nodeSelector: {{ toYaml . | indent 6 }} {{- end }} @@ -1222,6 +3226,10 @@ modules: {{- end }} {{- with .affinity }} affinity: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .resources }} + resources: {{ toYaml . | indent 6 }} {{- end }} {{- end }} @@ -1230,28 +3238,28 @@ modules: 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 }} + sessionProcessorsPerNode: {{ .sessionProcessorsPerNode }} + replicas: {{ .replicas | default 2 }} + subPath: {{ .subPath }} + storageClass: {{ .storageClass | default "nodemanager" }} + existingClaim: {{ .existingClaim }} + 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 }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} {{- end }} {{- end }} @@ -1276,6 +3284,7 @@ modules: affinity: {{ toYaml . | indent 6 }} {{- end }} + notebook_hashed_password: {{ .notebook_hashed_password | default "" }} {{- end }} @@ -1323,6 +3332,18 @@ modules: type: {{ .type }} username: {{ .username }} password: {{ .password }} + {{- with .nodeSelector }} + nodeSelector: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .tolerations }} + tolerations: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .affinity }} + affinity: +{{ toYaml . | indent 6 }} + {{- end }} {{- end}} spark: @@ -1350,6 +3371,7 @@ modules: {{ toYaml . | indent 8 }} {{- end }} type: {{ .master.type }} + nodePort: {{ .master.nodePort }} {{- end }} {{- if .worker }} worker: @@ -1398,8 +3420,9 @@ modules: existingClaim: {{ .namenode.existingClaim }} storageClass: {{ .namenode.storageClass | default "" }} accessMode: {{ .namenode.accessMode | default "ReadWriteOnce" }} - size: {{ .namenode.size }} + size: {{ .namenode.size | default "1Gi" }} datanode: + replicas: {{ .datanode.replicas | default 3 }} {{- with .datanode.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} @@ -1416,7 +3439,7 @@ modules: existingClaim: {{ .datanode.existingClaim }} storageClass: {{ .datanode.storageClass | default "" }} accessMode: {{ .datanode.accessMode | default "ReadWriteOnce" }} - size: {{ .datanode.size }} + size: {{ .datanode.size | default "1Gi" }} {{- end }} @@ -1456,6 +3479,10 @@ modules: rabbitmq: include: {{ has "rabbitmq" .modules }} {{- with .rabbitmq }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} {{- with .nodeSelector }} nodeSelector: {{ toYaml . | indent 6 }} @@ -1486,6 +3513,18 @@ modules: pulsar: include: {{ has "pulsar" .modules }} {{- with .pulsar }} + {{- with .resources }} + resources: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .env }} + env: +{{ toYaml . | indent 6 }} + {{- end }} + {{- with .confs }} + confs: +{{ toYaml . | indent 6 }} + {{- end }} {{- with .nodeSelector }} nodeSelector: {{ toYaml . | indent 6 }} @@ -1520,20 +3559,113 @@ modules: 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 }}`, - ArchiveContent: nil, - Private: false, +externalMysqlPassword: {{ .externalMysqlPassword }} +`, + Private: true, }, "242bf84c-548c-43d4-9f34-15f6d4dc0f33": { Model: gorm.Model{ - ID: 3, + ID: 5, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, @@ -1936,7 +4068,7 @@ modules: }, "8d1b15c1-cc7e-460b-8563-fa732457a049": { Model: gorm.Model{ - ID: 4, + ID: 6, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, diff --git a/server/infrastructure/gorm/mock/chart_fate_1_10_0.go b/server/infrastructure/gorm/mock/chart_fate_1_10_0.go new file mode 100644 index 0000000..75337cb --- /dev/null +++ b/server/infrastructure/gorm/mock/chart_fate_1_10_0.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 ( + FATE1100WithPortalChartArchiveContent = getFATE1100WithPortalChartArchiveContent() + FATEExchange1100WithManagerChartArchiveContent = getFATEExchange1100WithManagerChartArchiveContent() +) + +func getFATE1100WithPortalChartArchiveContent() []byte { + base64Content := `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` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} +func getFATEExchange1100WithManagerChartArchiveContent() []byte { + base64Content := `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` + content, _ := base64.StdEncoding.DecodeString(base64Content) + return content +} diff --git a/site-portal/.env b/site-portal/.env index 0ad57b4..89e0a1d 100644 --- a/site-portal/.env +++ b/site-portal/.env @@ -1,4 +1,4 @@ -TAG=v0.2.0 +TAG=v0.3.0 SERVER_NAME=federatedai/site-portal-server SERVER_IMG=${SERVER_NAME}:${TAG} diff --git a/site-portal/Makefile b/site-portal/Makefile index fc16202..41b7053 100644 --- a/site-portal/Makefile +++ b/site-portal/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean format swag swag-bin server-unittest server frontend run RELEASE_VERSION ?= ${shell git describe --tags} -TAG ?= v0.2.0 +TAG ?= v0.3.0 SERVER_NAME ?= federatedai/site-portal-server SERVER_IMG ?= ${SERVER_NAME}:${TAG} From 9e734fb330aaec2f8c54fd730806eeb14d7eebbb Mon Sep 17 00:00:00 2001 From: Chenlong Ma Date: Fri, 24 Mar 2023 15:23:59 +0800 Subject: [PATCH 03/16] FedLCM supports upgrading clusters (#30) * FedLCM supports upgrading fate clusters (#11) * FedLCM supports upgrading the FATE cluster server to upgrade the detection part Signed-off-by: Chenlong Ma * Add upgrade API Signed-off-by: Chenlong Ma * Fix swag install Signed-off-by: Chenlong Ma * swag fmt Signed-off-by: Chenlong Ma * Add FATE upgrade API Signed-off-by: Chenlong Ma * fix bug 'The upgraded FATE cannot run the job normally' Signed-off-by: Chenlong Ma * add version Signed-off-by: Chenlong Ma * FedLCM supports cross-version upgrade of FATE Signed-off-by: Chenlong Ma * update swagger Signed-off-by: Chenlong Ma * fix go test Signed-off-by: Chenlong Ma --------- Signed-off-by: Chenlong Ma * The front end supports the upgrade of exchange and cluster (#29) * Implement exchange and cluster version upgrades (#21) * Portal adds exchange and cluster upgrade functions Signed-off-by: qijianshuai * The cluster exchange interface displays the version Signed-off-by: qijianshuai * version text modify Signed-off-by: qijianshuai * Fix bugs in ui during cluster and exchange upgrade Signed-off-by: qijianshuai --------- Signed-off-by: qijianshuai Co-authored-by: qijianshuai Signed-off-by: qijianshuai * Protal adds a prompt that the version of the cluster is inconsistent with that of the exchange (#24) Signed-off-by: qijianshuai Co-authored-by: qijianshuai Signed-off-by: qijianshuai * portal fixed Signed-off-by: qijianshuai --------- Signed-off-by: qijianshuai Co-authored-by: qijianshuai Signed-off-by: qijianshuai <67683493+qijianshuai@users.noreply.github.com> * Refresh the portal fed and open detail pages, the content of Exposed Services appears repeatedly (#33) Signed-off-by: qijianshuai --------- Signed-off-by: Chenlong Ma Signed-off-by: qijianshuai <67683493+qijianshuai@users.noreply.github.com> Signed-off-by: qijianshuai Co-authored-by: qijianshuai <67683493+qijianshuai@users.noreply.github.com> Co-authored-by: qjianshuai <1096324583@qq.com> --- Makefile | 2 +- fml-manager/server/api/job.go | 52 +- fml-manager/server/api/project.go | 232 ++-- fml-manager/server/api/site.go | 40 +- fml-manager/server/main.go | 14 +- frontend/src/app/app-routing.module.ts | 9 + frontend/src/app/app.component.ts | 4 +- frontend/src/app/app.module.ts | 4 +- .../services/federation-fate/fed.service.ts | 8 + .../cluster-detail.component.html | 11 +- .../cluster-detail.component.ts | 9 + .../exchange-cluster-upgrade.component.html | 35 + .../exchange-cluster-upgrade.component.scss | 18 + ...exchange-cluster-upgrade.component.spec.ts | 25 + .../exchange-cluster-upgrade.component.ts | 65 + .../exchange-detail.component.html | 11 +- .../exchange-detail.component.ts | 9 + .../fed-detail-fate.component.html | 36 +- .../fed-detail-fate.component.ts | 17 +- .../fed-detail-openfl.component.ts | 1 + frontend/src/assets/i18n/en.json | 9 +- frontend/src/assets/i18n/zh_CN.json | 9 +- frontend/src/utils/constant.ts | 3 +- .../templates/core/fateflow/configmap.yaml | 6 +- pkg/kubefate/kubefate.go | 2 +- server/api/certificate-authority.go | 66 +- server/api/certificate.go | 32 +- server/api/chart.go | 34 +- server/api/endpoint.go | 135 ++- server/api/event.go | 15 +- server/api/federation.go | 496 +++++--- server/api/federation_openfl.go | 295 ++--- server/api/infra_provider.go | 106 +- server/api/user.go | 60 +- .../service/participant_service.go | 114 ++ server/docs/docs.go | 302 +++++ server/docs/swagger.json | 302 +++++ server/docs/swagger.yaml | 183 +++ server/domain/entity/participant_fate.go | 4 + server/domain/repo/chart_repo.go | 2 + .../service/participant_fate_service_test.go | 10 +- .../participant_fate_upgrade_service.go | 328 +++++ server/domain/utils/upgrade.go | 87 ++ server/domain/utils/upgrade_test.go | 83 ++ server/infrastructure/gorm/chart_mock_repo.go | 1050 +++++------------ .../gorm/mock/chart_fate_191.go | 33 - .../gorm/mock/chart_fate_1_10_0.go | 4 +- .../gorm/participant_fate_repo.go | 2 +- server/main.go | 15 +- site-portal/server/api/job.go | 224 ++-- site-portal/server/api/local_data.go | 130 +- site-portal/server/api/model.go | 96 +- site-portal/server/api/project.go | 550 ++++----- site-portal/server/api/site.go | 92 +- site-portal/server/api/user.go | 88 +- site-portal/server/main.go | 14 +- 56 files changed, 3485 insertions(+), 2098 deletions(-) create mode 100644 frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.html create mode 100644 frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.scss create mode 100644 frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.spec.ts create mode 100644 frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.ts create mode 100644 server/domain/service/participant_fate_upgrade_service.go create mode 100644 server/domain/utils/upgrade.go create mode 100644 server/domain/utils/upgrade_test.go delete mode 100644 server/infrastructure/gorm/mock/chart_fate_191.go diff --git a/Makefile b/Makefile index a89a798..73d0341 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ ifeq (, $(shell which swag)) SWAG_BIN_TMP_DIR=$$(mktemp -d) ;\ cd $$SWAG_BIN_TMP_DIR ;\ go mod init tmp ;\ - go get -u github.com/swaggo/swag/cmd/swag ;\ + go install github.com/swaggo/swag/cmd/swag@v1.8.7 ;\ rm -rf $$SWAG_BIN_TMP_DIR ;\ } SWAG_BIN=$(GOBIN)/swag diff --git a/fml-manager/server/api/job.go b/fml-manager/server/api/job.go index d46d435..2249d73 100644 --- a/fml-manager/server/api/job.go +++ b/fml-manager/server/api/job.go @@ -61,14 +61,14 @@ func (controller *JobController) Route(r *gin.RouterGroup) { } // 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] +// @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{} @@ -91,15 +91,15 @@ func (controller *JobController) handleJobCreation(c *gin.Context) { } // 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] +// @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") @@ -123,15 +123,15 @@ func (controller *JobController) handleJobResponse(c *gin.Context) { } // 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] +// @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") diff --git a/fml-manager/server/api/project.go b/fml-manager/server/api/project.go index 4be28e5..91e043c 100644 --- a/fml-manager/server/api/project.go +++ b/fml-manager/server/api/project.go @@ -76,14 +76,14 @@ func (controller *ProjectController) Route(r *gin.RouterGroup) { } // 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] +// @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{} @@ -106,14 +106,14 @@ func (controller *ProjectController) handleInvitation(c *gin.Context) { } // 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] +// @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") @@ -133,14 +133,14 @@ func (controller *ProjectController) handleInvitationAcceptance(c *gin.Context) } // 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] +// @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") @@ -160,14 +160,14 @@ func (controller *ProjectController) handleInvitationRejection(c *gin.Context) { } // 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] +// @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") @@ -187,14 +187,14 @@ func (controller *ProjectController) handleInvitationRevocation(c *gin.Context) } // 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] +// @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{} @@ -217,15 +217,15 @@ func (controller *ProjectController) handleParticipantInfoUpdate(c *gin.Context) } // 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] +// @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") @@ -249,15 +249,15 @@ func (controller *ProjectController) handleDataAssociation(c *gin.Context) { } // 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] +// @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") @@ -281,15 +281,15 @@ func (controller *ProjectController) handleDataDismissal(c *gin.Context) { } // 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] +// @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") @@ -310,15 +310,15 @@ func (controller *ProjectController) handleParticipantLeaving(c *gin.Context) { } // 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] +// @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") @@ -339,14 +339,14 @@ func (controller *ProjectController) handleParticipantDismissal(c *gin.Context) } // 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] +// @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") @@ -366,14 +366,14 @@ func (controller *ProjectController) handleProjectClosing(c *gin.Context) { } // 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] +// @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", "") @@ -395,14 +395,14 @@ func (controller *ProjectController) list(c *gin.Context) { } // 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] +// @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") @@ -424,14 +424,14 @@ func (controller *ProjectController) listData(c *gin.Context) { } // 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] +// @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") @@ -453,14 +453,14 @@ func (controller *ProjectController) listParticipant(c *gin.Context) { } // handleParticipantUnregistration process a participant unregistration event -// @Summary Process participant unregistration event, called by this FML manager's site context only -// @Tags Project -// @Produce json -// @Param site body event.ProjectParticipantUnregistrationEvent true "Unregistered site 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/unregister [post] +// @Summary Process participant unregistration event, called by this FML manager's site context only +// @Tags Project +// @Produce json +// @Param site body event.ProjectParticipantUnregistrationEvent true "Unregistered site 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/unregister [post] func (controller *ProjectController) handleParticipantUnregistration(c *gin.Context) { if err := func() error { unregistrationEvent := &event.ProjectParticipantUnregistrationEvent{} diff --git a/fml-manager/server/api/site.go b/fml-manager/server/api/site.go index bc689c6..e572528 100644 --- a/fml-manager/server/api/site.go +++ b/fml-manager/server/api/site.go @@ -53,12 +53,12 @@ func (controller *SiteController) Route(r *gin.RouterGroup) { } // 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] +// @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 { @@ -79,13 +79,13 @@ func (controller *SiteController) getSite(c *gin.Context) { } // 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] +// @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{} @@ -108,13 +108,13 @@ func (controller *SiteController) postSite(c *gin.Context) { } // deleteSite removes a site -// @Summary Remove a site, all related projects will be impacted -// @Tags Site -// @Produce json -// @Param uuid path string true "The site UUID" -// @Success 200 {object} GeneralResponse "Success" -// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" -// @Router /site/{uuid} [delete] +// @Summary Remove a site, all related projects will be impacted +// @Tags Site +// @Produce json +// @Param uuid path string true "The site UUID" +// @Success 200 {object} GeneralResponse "Success" +// @Failure 500 {object} GeneralResponse{code=int} "Internal server error" +// @Router /site/{uuid} [delete] func (controller *SiteController) deleteSite(c *gin.Context) { if err := func() error { siteUUID := c.Param("uuid") diff --git a/fml-manager/server/main.go b/fml-manager/server/main.go index f2cdf38..661094b 100644 --- a/fml-manager/server/main.go +++ b/fml-manager/server/main.go @@ -43,13 +43,13 @@ import ( ) // 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 +// @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(".", "_") diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 39f4dc4..c0a57fc 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -34,6 +34,7 @@ import { TimeOutServiceComponent } from './components/time-out-service/time-out- 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 { ExchangeClusterUpgradeComponent } from './view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component'; import { AuthService } from './services/common/auth.service'; import { RouterGuard } from './router-guard'; @@ -171,6 +172,14 @@ const routes: Routes = [ component: ClusterDetailComponent }, + { + path: 'federation/fate/:id/detail/:uuid/:version/:name/upgrade', + data: { + preload: true + }, + + component: ExchangeClusterUpgradeComponent + }, { path: 'federation/openfl/:id/envoy/detail/:envoy_uuid', data: { diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9f754d1..e012e6f 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -10,10 +10,10 @@ // 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 { 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, uploadIcon, warningStandardIcon} 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); +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, uploadIcon, warningStandardIcon); @Component({ selector: 'app-root', diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7cfa8b8..66a85d6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -53,6 +53,7 @@ import { TimeOutServiceComponent } from './components/time-out-service/time-out- 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'; +import { ExchangeClusterUpgradeComponent } from './view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component'; @NgModule({ declarations: [ @@ -85,7 +86,8 @@ import { CreateOpenflComponent } from './view/openfl/create-openfl-fed/create-op DirectorDetailComponent, FilterComponent, EnvoyDetailComponent, - CreateOpenflComponent + CreateOpenflComponent, + ExchangeClusterUpgradeComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/services/federation-fate/fed.service.ts b/frontend/src/app/services/federation-fate/fed.service.ts index d37da85..5a17cec 100644 --- a/frontend/src/app/services/federation-fate/fed.service.ts +++ b/frontend/src/app/services/federation-fate/fed.service.ts @@ -103,4 +103,12 @@ export class FedService { createExternalCluster(fed_uuid:string, externalCluster:any): Observable { return this.http.post('/federation/fate/'+ fed_uuid +'/cluster/external', externalCluster); } + + getExchangeClusterUpgradeVersionList(fed_uuid:string, upgrade_uuid: string, type: 'cluster' | 'exchange') { + return this.http.get(`/federation/fate/${fed_uuid}/${type}/${upgrade_uuid}/upgrade`) + } + + upgradeExchangeCluster(fed_uuid:string, upgrade_uuid: string, type: 'cluster' | 'exchange', data: {upgradeVersion: string}) { + return this.http.post(`/federation/fate/${fed_uuid}/${type}/${upgrade_uuid}/upgrade?upgradeVersion=${data.upgradeVersion}`, {}); + } } \ No newline at end of file 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 index 1d08ae3..9cad338 100644 --- a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.html +++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.html @@ -1,5 +1,5 @@
- <<{{'CommonlyUse.back'|translate}} + <<{{'CommonlyUse.back'|translate}}

{{'ClusterDetail.detail'|translate}}